Skip to content

Commit 850f8bb

Browse files
authoredApr 21, 2021
[KODO-12017] 添加生产模式输出详细日志功能及配置 (#499)
1 parent 4a1da8c commit 850f8bb

16 files changed

+458
-111
lines changed
 

‎.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
.DS_Store
2+
.vscode
23
node_modules
34
bower_components
45
demo/config.js

‎README.md

+1
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,7 @@ qiniu.compressImage(file, options).then(data => {
201201
* config.checkByMD5: 是否开启 MD5 校验,为布尔值;在断点续传时,开启 MD5 校验会将已上传的分片与当前分片进行 MD5 值比对,若不一致,则重传该分片,避免使用错误的分片。读取分片内容并计算 MD5 需要花费一定的时间,因此会稍微增加断点续传时的耗时,默认为 false,不开启。
202202
* config.forceDirect: 是否上传全部采用直传方式,为布尔值;为 `true` 时则上传方式全部为直传 form 方式,禁用断点续传,默认 `false`
203203
* config.chunkSize: `number`,分片上传时每片的大小,必须为正整数,单位为 `MB`,且最大不能超过 1024,默认值 4。因为 chunk 数最大 10000,所以如果文件以你所设的 `chunkSize` 进行分片并且 chunk 数超过 10000,我们会把你所设的 `chunkSize` 扩大二倍,如果仍不符合则继续扩大,直到符合条件。
204+
* config.debugLogLevel: `INFO` | `WARN` | `ERROR` | `OFF`,允许程序在控制台输出日志,默认为 `OFF`,不输出任何日志,本功能仅仅用于本地调试,不建议在线上环境开启。
204205

205206
* **putExtra**: `object`,其中的每一项都为可选
206207

‎src/index.ts

+4-5
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,9 @@
1-
import StatisticsLogger from './statisticsLog'
21
import createUploadManager, { Extra, Config, UploadOptions, UploadProgress } from './upload'
32
import { Observable, IObserver } from './observable'
43
import { CustomError } from './utils'
54
import { UploadCompleteData } from './api'
65
import compressImage from './compress'
7-
8-
const statisticsLogger = new StatisticsLogger()
6+
import Logger from './logger'
97

108
/**
119
* @param file 上传文件
@@ -22,7 +20,6 @@ function upload(
2220
putExtra?: Partial<Extra>,
2321
config?: Partial<Config>
2422
): Observable<UploadProgress, CustomError, UploadCompleteData> {
25-
2623
const options: UploadOptions = {
2724
file,
2825
key,
@@ -31,12 +28,14 @@ function upload(
3128
config
3229
}
3330

31+
// 为每个任务创建单独的 Logger
32+
const logger = new Logger(token, config?.disableStatisticsReport, config?.debugLogLevel)
3433
return new Observable((observer: IObserver<UploadProgress, CustomError, UploadCompleteData>) => {
3534
const manager = createUploadManager(options, {
3635
onData: (data: UploadProgress) => observer.next(data),
3736
onError: (err: CustomError) => observer.error(err),
3837
onComplete: (res: any) => observer.complete(res)
39-
}, statisticsLogger)
38+
}, logger)
4039
manager.putFile()
4140
return manager.stop.bind(manager)
4241
})

‎src/logger/index.test.ts

+100
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
import Logger from './index'
2+
3+
let isCallReport = false
4+
5+
jest.mock('./report-v3', () => ({
6+
reportV3: () => {
7+
isCallReport = true
8+
}
9+
}))
10+
11+
const originalLog = console.log
12+
const originalWarn = console.warn
13+
const originalError = console.error
14+
15+
const logMessage: unknown[] = []
16+
const warnMessage: unknown[] = []
17+
const errorMessage: unknown[] = []
18+
19+
beforeAll(() => {
20+
console.log = jest.fn((...args: unknown[]) => logMessage.push(...args))
21+
console.warn = jest.fn((...args: unknown[]) => warnMessage.push(...args))
22+
console.error = jest.fn((...args: unknown[]) => errorMessage.push(...args))
23+
})
24+
25+
afterAll(() => {
26+
console.log = originalLog
27+
console.warn = originalWarn
28+
console.error = originalError
29+
})
30+
31+
describe('test logger', () => {
32+
test('test level', () => {
33+
const infoLogger = new Logger('', true, 'INFO')
34+
infoLogger.info('test1')
35+
expect(logMessage).toStrictEqual([`Qiniu-JS-SDK [INFO][1]: `, 'test1'])
36+
infoLogger.warn('test2')
37+
expect(warnMessage).toStrictEqual(['Qiniu-JS-SDK [WARN][1]: ', 'test2'])
38+
infoLogger.error('test3')
39+
expect(errorMessage).toStrictEqual(['Qiniu-JS-SDK [ERROR][1]: ', 'test3'])
40+
41+
// 清空消息
42+
logMessage.splice(0, logMessage.length)
43+
warnMessage.splice(0, warnMessage.length)
44+
errorMessage.splice(0, errorMessage.length)
45+
46+
const warnLogger = new Logger('', true, 'WARN')
47+
warnLogger.info('test1')
48+
expect(logMessage).toStrictEqual([])
49+
warnLogger.warn('test2')
50+
expect(warnMessage).toStrictEqual(['Qiniu-JS-SDK [WARN][2]: ', 'test2'])
51+
warnLogger.error('test3')
52+
expect(errorMessage).toStrictEqual(['Qiniu-JS-SDK [ERROR][2]: ', 'test3'])
53+
54+
// 清空消息
55+
logMessage.splice(0, logMessage.length)
56+
warnMessage.splice(0, warnMessage.length)
57+
errorMessage.splice(0, errorMessage.length)
58+
59+
const errorLogger = new Logger('', true, 'ERROR')
60+
errorLogger.info('test1')
61+
expect(logMessage).toStrictEqual([])
62+
errorLogger.warn('test2')
63+
expect(warnMessage).toStrictEqual([])
64+
errorLogger.error('test3')
65+
expect(errorMessage).toStrictEqual(['Qiniu-JS-SDK [ERROR][3]: ', 'test3'])
66+
67+
// 清空消息
68+
logMessage.splice(0, logMessage.length)
69+
warnMessage.splice(0, warnMessage.length)
70+
errorMessage.splice(0, errorMessage.length)
71+
72+
const offLogger = new Logger('', true, 'OFF')
73+
offLogger.info('test1')
74+
expect(logMessage).toStrictEqual([])
75+
offLogger.warn('test2')
76+
expect(warnMessage).toStrictEqual([])
77+
offLogger.error('test3')
78+
expect(errorMessage).toStrictEqual([])
79+
})
80+
81+
test('test unique id', () => {
82+
// @ts-ignore
83+
const startId = Logger.id
84+
new Logger('', true, 'OFF')
85+
new Logger('', true, 'OFF')
86+
const last = new Logger('', true, 'OFF')
87+
// @ts-ignore
88+
expect(last.id).toStrictEqual(startId + 3)
89+
})
90+
91+
test('test report', () => {
92+
const logger1 = new Logger('', false, 'OFF')
93+
logger1.report(null as any)
94+
expect(isCallReport).toBeTruthy()
95+
isCallReport = false
96+
const logger2 = new Logger('', true, 'OFF')
97+
logger2.report(null as any)
98+
expect(isCallReport).toBeFalsy()
99+
})
100+
})

‎src/logger/index.ts

+61
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import { reportV3, V3LogInfo } from './report-v3'
2+
3+
export type LogLevel = 'INFO' | 'WARN' | 'ERROR' | 'OFF'
4+
5+
export default class Logger {
6+
private static id: number = 0
7+
8+
// 为每个类分配一个 id
9+
// 用以区分不同的上传任务
10+
private id = ++Logger.id
11+
12+
constructor(
13+
private token: string,
14+
private disableReport = true,
15+
private level: LogLevel = 'OFF'
16+
) { }
17+
18+
/**
19+
* @param {V3LogInfo} data 上报的数据。
20+
* @param {boolean} retry 重试次数,可选,默认为 3。
21+
* @description 向服务端上报统计信息。
22+
*/
23+
report(data: V3LogInfo, retry?: number) {
24+
if (this.disableReport) return
25+
try { reportV3(this.token, data, retry) }
26+
catch (error) { console.warn(error) }
27+
}
28+
29+
/**
30+
* @param {unknown[]} ...args
31+
* @description 输出 info 级别的调试信息。
32+
*/
33+
info(...args: unknown[]) {
34+
const allowLevel: LogLevel[] = ['INFO']
35+
if (allowLevel.includes(this.level)) {
36+
console.log(`Qiniu-JS-SDK [INFO][${this.id}]: `, ...args)
37+
}
38+
}
39+
40+
/**
41+
* @param {unknown[]} ...args
42+
* @description 输出 warn 级别的调试信息。
43+
*/
44+
warn(...args: unknown[]) {
45+
const allowLevel: LogLevel[] = ['INFO', 'WARN']
46+
if (allowLevel.includes(this.level)) {
47+
console.warn(`Qiniu-JS-SDK [WARN][${this.id}]: `, ...args)
48+
}
49+
}
50+
51+
/**
52+
* @param {unknown[]} ...args
53+
* @description 输出 error 级别的调试信息。
54+
*/
55+
error(...args: unknown[]) {
56+
const allowLevel: LogLevel[] = ['INFO', 'WARN', 'ERROR']
57+
if (allowLevel.includes(this.level)) {
58+
console.error(`Qiniu-JS-SDK [ERROR][${this.id}]: `, ...args)
59+
}
60+
}
61+
}

‎src/logger/report-v3.test.ts

+103
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
import { reportV3, V3LogInfo } from './report-v3'
2+
3+
class MockXHR {
4+
sendData: string
5+
openData: string[]
6+
openCount: number
7+
headerData: string[]
8+
9+
status: number
10+
readyState: number
11+
onreadystatechange() { }
12+
13+
clear() {
14+
this.sendData = ''
15+
this.openData = []
16+
this.headerData = []
17+
18+
this.status = 0
19+
this.readyState = 0
20+
}
21+
22+
open(...args: string[]) {
23+
this.clear()
24+
this.openCount += 1
25+
this.openData = args
26+
}
27+
28+
send(args: string) {
29+
this.sendData = args
30+
}
31+
32+
setRequestHeader(...args: string[]) {
33+
this.headerData.push(...args)
34+
}
35+
36+
changeStatusAndState(readyState: number, status: number) {
37+
this.status = status
38+
this.readyState = readyState
39+
this.onreadystatechange()
40+
}
41+
}
42+
43+
const mockXHR = new MockXHR()
44+
45+
jest.mock('../utils', () => ({
46+
createXHR: () => mockXHR,
47+
getAuthHeaders: (t: string) => t
48+
}))
49+
50+
describe('test report-v3', () => {
51+
const testData: V3LogInfo = {
52+
code: 200,
53+
reqId: 'reqId',
54+
host: 'host',
55+
remoteIp: 'remoteIp',
56+
port: 'port',
57+
duration: 1,
58+
time: 1,
59+
bytesSent: 1,
60+
upType: 'jssdk-h5',
61+
size: 1
62+
}
63+
64+
test('test stringify send Data', () => {
65+
reportV3('token', testData, 3)
66+
mockXHR.changeStatusAndState(0, 0)
67+
expect(mockXHR.sendData).toBe([
68+
testData.code || '',
69+
testData.reqId || '',
70+
testData.host || '',
71+
testData.remoteIp || '',
72+
testData.port || '',
73+
testData.duration || '',
74+
testData.time || '',
75+
testData.bytesSent || '',
76+
testData.upType || '',
77+
testData.size || ''
78+
].join(','))
79+
})
80+
81+
test('test retry', () => {
82+
mockXHR.openCount = 0
83+
reportV3('token', testData)
84+
for (let index = 1; index <= 10; index++) {
85+
mockXHR.changeStatusAndState(4, 0)
86+
}
87+
expect(mockXHR.openCount).toBe(4)
88+
89+
mockXHR.openCount = 0
90+
reportV3('token', testData, 4)
91+
for (let index = 1; index < 10; index++) {
92+
mockXHR.changeStatusAndState(4, 0)
93+
}
94+
expect(mockXHR.openCount).toBe(5)
95+
96+
mockXHR.openCount = 0
97+
reportV3('token', testData, 0)
98+
for (let index = 1; index < 10; index++) {
99+
mockXHR.changeStatusAndState(4, 0)
100+
}
101+
expect(mockXHR.openCount).toBe(1)
102+
})
103+
})

‎src/logger/report-v3.ts

+48
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import { createXHR, getAuthHeaders } from '../utils'
2+
3+
export interface V3LogInfo {
4+
code: number
5+
reqId: string
6+
host: string
7+
remoteIp: string
8+
port: string
9+
duration: number
10+
time: number
11+
bytesSent: number
12+
upType: 'jssdk-h5'
13+
size: number
14+
}
15+
16+
/**
17+
* @param {string} token 上传使用的 token
18+
* @param {V3LogInfo} data 上报的统计数据
19+
* @param {number} retry 重试的次数,默认值 3
20+
* @description v3 版本的日志上传接口,参考文档 https://github.com/qbox/product/blob/master/kodo/uplog.md#%E7%89%88%E6%9C%AC-3。
21+
*/
22+
export function reportV3(token: string, data: V3LogInfo, retry = 3) {
23+
const xhr = createXHR()
24+
xhr.open('POST', 'https://uplog.qbox.me/log/3')
25+
xhr.setRequestHeader('Content-type', 'application/x-www-form-urlencoded')
26+
xhr.setRequestHeader('Authorization', getAuthHeaders(token).Authorization)
27+
xhr.onreadystatechange = () => {
28+
if (xhr.readyState === 4 && xhr.status !== 200 && retry > 0) {
29+
reportV3(token, data, retry - 1)
30+
}
31+
}
32+
33+
// 顺序参考:https://github.com/qbox/product/blob/master/kodo/uplog.md#%E7%89%88%E6%9C%AC-3
34+
const stringifyData = [
35+
data.code || '',
36+
data.reqId || '',
37+
data.host || '',
38+
data.remoteIp || '',
39+
data.port || '',
40+
data.duration || '',
41+
data.time || '',
42+
data.bytesSent || '',
43+
data.upType || '',
44+
data.size || ''
45+
].join(',')
46+
47+
xhr.send(stringifyData)
48+
}

‎src/statisticsLog.ts

-36
This file was deleted.

‎src/upload/base.ts

+50-30
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
1-
import * as utils from '../utils'
2-
import { getUploadUrl, UploadCompleteData } from '../api'
3-
4-
import StatisticsLogger from '../statisticsLog'
51
import { region } from '../config'
2+
import { getUploadUrl } from '../api'
3+
import Logger, { LogLevel } from '../logger'
4+
import * as utils from '../utils'
65

76
export const DEFAULT_CHUNK_SIZE = 4 // 单位 MB
87

@@ -32,14 +31,16 @@ export interface Config {
3231
uphost: string
3332
/** 自定义分片上传并发请求量 */
3433
concurrentRequestLimit: number
35-
/** 是否禁止静态日志上报 */
36-
disableStatisticsReport: boolean
3734
/** 分片大小,单位为 MB */
3835
chunkSize: number
3936
/** 上传域名协议 */
4037
upprotocol: 'http:' | 'https:'
4138
/** 上传区域 */
4239
region?: typeof region[keyof typeof region]
40+
/** 是否禁止统计日志上报 */
41+
disableStatisticsReport: boolean
42+
/** 设置调试日志输出模式,默认 `OFF`,不输出任何日志 */
43+
debugLogLevel?: LogLevel
4344
}
4445

4546
export interface UploadOptions {
@@ -103,7 +104,7 @@ export default abstract class Base {
103104

104105
protected abstract run(): utils.Response<any>
105106

106-
constructor(options: UploadOptions, handlers: UploadHandler, private statisticsLogger: StatisticsLogger) {
107+
constructor(options: UploadOptions, handlers: UploadHandler, protected logger: Logger) {
107108
this.config = {
108109
useCdnDomain: true,
109110
disableStatisticsReport: false,
@@ -117,69 +118,81 @@ export default abstract class Base {
117118
...options.config
118119
}
119120

121+
logger.info('config inited.', this.config)
122+
120123
this.putExtra = {
121124
fname: '',
122125
...options.putExtra
123126
}
124127

128+
logger.info('putExtra inited.', this.putExtra)
129+
125130
this.file = options.file
126131
this.key = options.key
127132
this.token = options.token
128133

129134
this.onData = handlers.onData
130135
this.onError = handlers.onError
131136
this.onComplete = handlers.onComplete
137+
132138
try {
133139
this.bucket = utils.getPutPolicy(this.token).bucket
134140
} catch (e) {
141+
logger.error('get bucket from token failed.', e)
135142
this.onError(e)
136143
}
137144
}
138145

139-
public async putFile(): Promise<utils.ResponseSuccess<UploadCompleteData>> {
146+
private handleError(message: string) {
147+
const err = new Error(message)
148+
this.logger.error(message)
149+
this.onError(err)
150+
}
151+
152+
/**
153+
* @returns Promise 返回结果与上传最终状态无关,状态信息请通过 [Subscriber] 获取。
154+
* @description 上传文件,状态信息请通过 [Subscriber] 获取。
155+
*/
156+
public async putFile(): Promise<void> {
140157
this.aborted = false
141158
if (!this.putExtra.fname) {
159+
this.logger.info('use file.name as fname.')
142160
this.putExtra.fname = this.file.name
143161
}
144162

145163
if (this.file.size > 10000 * GB) {
146-
const err = new Error('file size exceed maximum value 10000G')
147-
this.onError(err)
148-
throw err
164+
this.handleError('file size exceed maximum value 10000G.')
165+
return
149166
}
150167

151168
if (this.putExtra.customVars) {
152169
if (!utils.isCustomVarsValid(this.putExtra.customVars)) {
153-
const err = new Error('customVars key should start width x:')
154-
this.onError(err)
155-
throw err
170+
this.handleError('customVars key should start width x:.')
171+
return
156172
}
157173
}
158174

159175
if (this.putExtra.metadata) {
160176
if (!utils.isMetaDataValid(this.putExtra.metadata)) {
161-
const err = new Error('metadata key should start with x-qn-meta-')
162-
this.onError(err)
163-
throw err
177+
this.handleError('metadata key should start with x-qn-meta-.')
178+
return
164179
}
165180
}
166181

167182
try {
168183
this.uploadUrl = await getUploadUrl(this.config, this.token)
184+
this.logger.info('get uploadUrl from api.', this.uploadUrl)
169185
this.uploadAt = new Date().getTime()
170186

171187
const result = await this.run()
172188
this.onComplete(result.data)
173-
174-
if (!this.config.disableStatisticsReport) {
175-
this.sendLog(result.reqId, 200)
176-
}
177-
178-
return result
179-
189+
this.sendLog(result.reqId, 200)
190+
return
180191
} catch (err) {
192+
this.logger.error(err)
193+
181194
this.clear()
182-
if (err.isRequestError && !this.config.disableStatisticsReport) {
195+
if (err.isRequestError) {
183196
const reqId = this.aborted ? '' : err.reqId
184197
const code = this.aborted ? -2 : err.code
185198
this.sendLog(reqId, code)
@@ -191,20 +204,27 @@ export default abstract class Base {
191204
// 1. 满足 needRetry 的条件且 retryCount 不为 0
192205
// 2. uploadId 无效时在 resume 里会清除本地数据,并且这里触发重新上传
193206
if (needRetry && notReachRetryCount || err.code === 612) {
194-
return this.putFile()
207+
this.logger.warn(`error auto retry: ${this.retryCount}/${this.config.retryCount}.`)
208+
this.putFile()
209+
return
195210
}
196211

197212
this.onError(err)
198-
throw err
199213
}
200214
}
201215

202216
private clear() {
203-
this.xhrList.forEach(xhr => xhr.abort())
217+
this.logger.info('start cleaning all xhr.')
218+
this.xhrList.forEach(xhr => {
219+
xhr.onreadystatechange = null
220+
xhr.abort()
221+
})
222+
this.logger.info('cleanup completed.')
204223
this.xhrList = []
205224
}
206225

207226
public stop() {
227+
this.logger.info('stop.')
208228
this.clear()
209229
this.aborted = true
210230
}
@@ -214,7 +234,7 @@ export default abstract class Base {
214234
}
215235

216236
private sendLog(reqId: string, code: number) {
217-
this.statisticsLogger.log({
237+
this.logger.report({
218238
code,
219239
reqId,
220240
host: utils.getDomainFromUrl(this.uploadUrl),
@@ -225,7 +245,7 @@ export default abstract class Base {
225245
bytesSent: this.progress ? this.progress.total.loaded : 0,
226246
upType: 'jssdk-h5',
227247
size: this.file.size
228-
}, this.token)
248+
})
229249
}
230250

231251
public getProgressInfoItem(loaded: number, size: number) {

‎src/upload/direct.ts

+7
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import { UploadCompleteData } from '../api'
55
export default class Direct extends Base {
66

77
protected async run() {
8+
this.logger.info('start run Direct.')
9+
810
const formData = new FormData()
911
formData.append('file', this.file)
1012
formData.append('token', this.token)
@@ -14,10 +16,13 @@ export default class Direct extends Base {
1416
formData.append('fname', this.putExtra.fname)
1517

1618
if (this.putExtra.customVars) {
19+
this.logger.info('init customVars.')
1720
const { customVars } = this.putExtra
1821
Object.keys(customVars).forEach(key => formData.append(key, customVars[key].toString()))
22+
this.logger.info('customVars inited.')
1923
}
2024

25+
this.logger.info('formData inited.')
2126
const result = await request<UploadCompleteData>(this.uploadUrl, {
2227
method: 'POST',
2328
body: formData,
@@ -27,6 +32,7 @@ export default class Direct extends Base {
2732
onCreate: xhr => this.addXhr(xhr)
2833
})
2934

35+
this.logger.info('Direct progress finish.')
3036
this.finishDirectProgress()
3137
return result
3238
}
@@ -40,6 +46,7 @@ export default class Direct extends Base {
4046
private finishDirectProgress() {
4147
// 在某些浏览器环境下,xhr 的 progress 事件无法被触发,progress 为 null,这里 fake 下
4248
if (!this.progress) {
49+
this.logger.warn('progress is null.')
4350
this.progress = { total: this.getProgressInfoItem(this.file.size, this.file.size) }
4451
this.onData(this.progress)
4552
return

‎src/upload/index.ts

+11-6
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import Resume from './resume'
22
import Direct from './direct'
33
import { UploadOptions, UploadHandler } from './base'
4-
import StatisticsLogger from '../statisticsLog'
4+
import Logger from '../logger'
55
import { MB } from '../utils'
66

77
export * from './base'
@@ -10,13 +10,18 @@ export * from './resume'
1010
export default function createUploadManager(
1111
options: UploadOptions,
1212
handlers: UploadHandler,
13-
statisticsLogger: StatisticsLogger
13+
logger: Logger
1414
) {
1515
if (options.config && options.config.forceDirect) {
16-
return new Direct(options, handlers, statisticsLogger)
16+
logger.info('ues forceDirect mode.')
17+
return new Direct(options, handlers, logger)
1718
}
1819

19-
return options.file.size > 4 * MB
20-
? new Resume(options, handlers, statisticsLogger)
21-
: new Direct(options, handlers, statisticsLogger)
20+
if (options.file.size > 4 * MB) {
21+
logger.info('file size over 4M, use Resume.')
22+
return new Resume(options, handlers, logger)
23+
}
24+
25+
logger.info('file size less or equal than 4M, use Direct.')
26+
return new Direct(options, handlers, logger)
2227
}

‎src/upload/resume.ts

+50-16
Original file line numberDiff line numberDiff line change
@@ -46,31 +46,46 @@ export default class Resume extends Base {
4646
private uploadId: string
4747

4848
protected async run() {
49+
this.logger.info('start run Resume.')
4950
if (!this.config.chunkSize || !isPositiveInteger(this.config.chunkSize)) {
50-
throw new Error('chunkSize must be a positive integer')
51+
const errorMessage = 'chunkSize must be a positive integer.'
52+
this.logger.error(errorMessage)
53+
throw new Error(errorMessage)
5154
}
5255

5356
if (this.config.chunkSize > 1024) {
54-
throw new Error('chunkSize maximum value is 1024')
57+
const errorMessage = 'chunkSize maximum value is 1024.'
58+
this.logger.error(errorMessage)
59+
throw new Error(errorMessage)
5560
}
5661

57-
await this.initBeforeUploadChunks()
62+
try {
63+
await this.initBeforeUploadChunks()
64+
} catch (error) {
65+
const errorMessage = 'initBeforeUploadChunks failed.'
66+
this.logger.warn(errorMessage, error)
67+
}
5868

5969
const pool = new Pool(
6070
(chunkInfo: ChunkInfo) => this.uploadChunk(chunkInfo),
6171
this.config.concurrentRequestLimit
6272
)
73+
74+
const localKey = this.getLocalKey()
6375
const uploadChunks = this.chunks.map((chunk, index) => pool.enqueue({ chunk, index }))
6476

6577
const result = Promise.all(uploadChunks).then(() => this.mkFileReq())
6678
result.then(
6779
() => {
68-
utils.removeLocalFileInfo(this.getLocalKey())
80+
try { utils.removeLocalFileInfo(localKey) }
81+
catch (error) { this.logger.error(error) }
6982
},
7083
err => {
84+
this.logger.error('uploadChunks failed.', err)
7185
// uploadId 无效,上传参数有误(多由于本地存储信息的 uploadId 失效
7286
if (err.code === 612 || err.code === 400) {
73-
utils.removeLocalFileInfo(this.getLocalKey())
87+
try { utils.removeLocalFileInfo(localKey) }
88+
catch (error) { this.logger.error(error) }
7489
}
7590
}
7691
)
@@ -80,6 +95,7 @@ export default class Resume extends Base {
8095
private async uploadChunk(chunkInfo: ChunkInfo) {
8196
const { index, chunk } = chunkInfo
8297
const info = this.uploadedList[index]
98+
this.logger.info(`upload part ${index}.`, info)
8399

84100
const shouldCheckMD5 = this.config.checkByMD5
85101
const reuseSaved = () => {
@@ -92,6 +108,7 @@ export default class Resume extends Base {
92108
}
93109

94110
const md5 = await utils.computeMd5(chunk)
111+
this.logger.info(`computed part md5.`, md5)
95112

96113
if (info && md5 === info.md5) {
97114
reuseSaved()
@@ -108,13 +125,15 @@ export default class Resume extends Base {
108125
onCreate: (xhr: XMLHttpRequest) => this.addXhr(xhr)
109126
}
110127

128+
this.logger.info(`part ${index} start uploading.`)
111129
const response = await uploadChunk(
112130
this.token,
113131
this.key,
114132
chunkInfo.index + 1,
115133
this.getUploadInfo(),
116134
requestOptions
117135
)
136+
this.logger.info(`part ${index} upload completed.`)
118137

119138
// 在某些浏览器环境下,xhr 的 progress 事件无法被触发,progress 为 null,这里在每次分片上传完成后都手动更新下 progress
120139
onProgress({
@@ -128,11 +147,14 @@ export default class Resume extends Base {
128147
size: chunk.size
129148
}
130149

131-
utils.setLocalFileInfo(this.getLocalKey(), {
132-
id: this.uploadId,
133-
data: this.uploadedList
134-
})
135-
150+
try {
151+
utils.setLocalFileInfo(this.getLocalKey(), {
152+
id: this.uploadId,
153+
data: this.uploadedList
154+
})
155+
} catch (error) {
156+
this.logger.info(`set part ${index} cache failed.`, error)
157+
}
136158
}
137159

138160
private async mkFileReq() {
@@ -146,7 +168,8 @@ export default class Resume extends Base {
146168
...this.putExtra.customVars && { customVars: this.putExtra.customVars },
147169
...this.putExtra.metadata && { metadata: this.putExtra.metadata }
148170
}
149-
171+
172+
this.logger.info('parts upload completed, make file.', data)
150173
const result = await uploadComplete(
151174
this.token,
152175
this.key,
@@ -156,23 +179,35 @@ export default class Resume extends Base {
156179
body: JSON.stringify(data)
157180
}
158181
)
182+
183+
this.logger.info('finishResumeProgress.')
159184
this.updateMkFileProgress(1)
160185
return result
161186
}
162187

163188
private async initBeforeUploadChunks() {
164-
const localInfo = utils.getLocalFileInfo(this.getLocalKey())
189+
let localInfo: LocalInfo | null = null
190+
try { localInfo = utils.getLocalFileInfo(this.getLocalKey()) }
191+
catch (error) { this.logger.warn(error) }
192+
165193
// 分片必须和当时使用的 uploadId 配套,所以断点续传需要把本地存储的 uploadId 拿出来
166194
// 假如没有 localInfo 本地信息并重新获取 uploadId
167195
if (!localInfo) {
168-
// 防止本地信息已被破坏,初始化时 clear 一下
169-
utils.removeLocalFileInfo(this.getLocalKey())
196+
this.logger.info('resume upload parts from api.')
170197
const res = await initUploadParts(this.token, this.bucket, this.key, this.uploadUrl)
198+
this.logger.info(`resume upload parts of id: ${res.data.uploadId}.`)
171199
this.uploadId = res.data.uploadId
172200
this.uploadedList = []
173201
} else {
174-
this.uploadId = localInfo.id
202+
const infoMessage = [
203+
'resume upload parts from local cache',
204+
`total ${localInfo.data.length} part`,
205+
`id is ${localInfo.id}.`
206+
]
207+
208+
this.logger.info(infoMessage.join(', '))
175209
this.uploadedList = localInfo.data
210+
this.uploadId = localInfo.id
176211
}
177212

178213
this.chunks = utils.getChunks(this.file, this.config.chunkSize)
@@ -220,5 +255,4 @@ export default class Resume extends Base {
220255
}
221256
this.onData(this.progress)
222257
}
223-
224258
}

‎src/utils.ts

+17-16
Original file line numberDiff line numberDiff line change
@@ -46,10 +46,7 @@ export function setLocalFileInfo(localKey: string, info: LocalInfo) {
4646
try {
4747
localStorage.setItem(localKey, JSON.stringify(info))
4848
} catch (err) {
49-
if (window.console && window.console.warn) {
50-
// eslint-disable-next-line no-console
51-
console.warn('setLocalFileInfo failed')
52-
}
49+
throw new Error(`setLocalFileInfo failed: ${localKey}`)
5350
}
5451
}
5552

@@ -62,24 +59,28 @@ export function removeLocalFileInfo(localKey: string) {
6259
try {
6360
localStorage.removeItem(localKey)
6461
} catch (err) {
65-
if (window.console && window.console.warn) {
66-
// eslint-disable-next-line no-console
67-
console.warn('removeLocalFileInfo failed')
68-
}
62+
throw new Error(`removeLocalFileInfo failed. key: ${localKey}`)
6963
}
7064
}
7165

7266
export function getLocalFileInfo(localKey: string): LocalInfo | null {
73-
try {
74-
const localInfo = localStorage.getItem(localKey)
75-
return localInfo ? JSON.parse(localInfo) : null
76-
} catch (err) {
77-
if (window.console && window.console.warn) {
78-
// eslint-disable-next-line no-console
79-
console.warn('getLocalFileInfo failed')
80-
}
67+
let localInfoString: string | null = null
68+
try { localInfoString = localStorage.getItem(localKey) }
69+
catch { throw new Error(`getLocalFileInfo failed. key: ${localKey}`) }
70+
71+
if (localInfoString == null) {
8172
return null
8273
}
74+
75+
let localInfo: LocalInfo | null = null
76+
try { localInfo = JSON.parse(localInfoString) }
77+
catch {
78+
// 本地信息已被破坏,直接删除
79+
removeLocalFileInfo(localKey)
80+
throw new Error(`getLocalFileInfo failed to parse. key: ${localKey}`)
81+
}
82+
83+
return localInfo
8384
}
8485

8586
export function getAuthHeaders(token: string) {

‎test/demo1/main.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@
55
useCdnDomain: true,
66
disableStatisticsReport: false,
77
retryCount: 6,
8-
region: qiniu.region.z2
8+
region: qiniu.region.z2,
9+
debugLogLevel: 'INFO'
910
};
1011
var putExtra = {
1112
customVars: {}

‎test/demo2/index.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@ let initFileInput = (res) =>{
88

99
let config = {
1010
useCdnDomain: true,
11-
region: qiniu.region.z2
11+
region: qiniu.region.z2,
12+
debugLogLevel: 'INFO'
1213
};
1314
let putExtra = {
1415
fname: "",

‎tsconfig.json

+1
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
},
2424
"exclude": [
2525
"node_modules",
26+
"examples",
2627
"coverage",
2728
"test",
2829
"dist",

0 commit comments

Comments
 (0)
Please sign in to comment.