• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

atinc / ngx-tethys / 68ef226c-f83e-44c1-b8ed-e420a83c5d84

28 May 2025 10:31AM UTC coverage: 10.352% (-80.0%) from 90.316%
68ef226c-f83e-44c1-b8ed-e420a83c5d84

Pull #3460

circleci

pubuzhixing8
chore: xxx
Pull Request #3460: refactor(icon): migrate signal input #TINFR-1476

132 of 6823 branches covered (1.93%)

Branch coverage included in aggregate %.

10 of 14 new or added lines in 1 file covered. (71.43%)

11648 existing lines in 344 files now uncovered.

2078 of 14525 relevant lines covered (14.31%)

6.69 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

7.09
/src/upload/upload.service.ts
1
import { coerceArray, humanizeBytes, TinyDate } from 'ngx-tethys/util';
2
import { from, Observable, Subscriber } from 'rxjs';
3
import { map, mergeMap, tap } from 'rxjs/operators';
4

5
import { XhrFactory } from '@angular/common';
6
import { inject, Injectable } from '@angular/core';
7

8
export enum ThyUploadStatus {
1✔
9
    pending = 'pending',
1✔
10
    started = 'started',
1✔
11
    uploading = 'uploading',
1✔
12
    done = 'done'
1✔
13
}
2✔
14

15
export interface ThyUploadResponse {
16
    status: ThyUploadStatus;
17
    uploadFile?: ThyUploadFile;
18
}
1✔
19

UNCOV
20
/**
×
21
 * 文件上传进度
22
 * @public
UNCOV
23
 * @order 40
×
24
 */
25
export interface ThyUploadFileProgress {
UNCOV
26
    /**
×
UNCOV
27
     * 上传状态
×
UNCOV
28
     * @default pending
×
29
     */
30
    status: ThyUploadStatus;
31

32
    /**
33
     * 进度百分比
34
     */
35
    percentage: number;
36

UNCOV
37
    /**
×
UNCOV
38
     * 上传速度
×
UNCOV
39
     */
×
UNCOV
40
    speed?: number;
×
UNCOV
41

×
42
    /**
43
     * 人类可读的上传速度,比如: 120 kb/s
44
     */
45
    speedHuman?: string;
UNCOV
46

×
UNCOV
47
    /**
×
UNCOV
48
     * 开始上传时间
×
UNCOV
49
     */
×
UNCOV
50
    startTime: number | null;
×
51

UNCOV
52
    /**
×
UNCOV
53
     * 结束上传时间
×
UNCOV
54
     */
×
UNCOV
55
    endTime?: number | null;
×
UNCOV
56

×
UNCOV
57
    /**
×
UNCOV
58
     * 上传时间
×
UNCOV
59
     */
×
UNCOV
60
    estimatedTime?: number | null;
×
UNCOV
61

×
UNCOV
62
    /**
×
UNCOV
63
     * 人类可读的上传时间,比如: 00:12:23
×
64
     */
65
    estimatedTimeHuman?: string | null;
UNCOV
66
}
×
UNCOV
67

×
UNCOV
68
/**
×
UNCOV
69
 * 文件上传对象
×
UNCOV
70
 * @public
×
UNCOV
71
 * @order 30
×
UNCOV
72
 */
×
UNCOV
73
export interface ThyUploadFile {
×
UNCOV
74
    /**
×
UNCOV
75
     * 文件唯一标识
×
UNCOV
76
     */
×
UNCOV
77
    identifier?: string;
×
UNCOV
78

×
UNCOV
79
    /**
×
80
     * 上传方法
81
     */
UNCOV
82
    method: string;
×
83

84
    /**
UNCOV
85
     * 远程上传地址
×
UNCOV
86
     */
×
87
    url: string;
88

UNCOV
89
    /**
×
UNCOV
90
     * 是否携带认证信息
×
91
     */
92
    withCredentials?: boolean;
93

94
    /**
UNCOV
95
     * 原始文件
×
UNCOV
96
     */
×
UNCOV
97
    nativeFile: File;
×
UNCOV
98

×
UNCOV
99
    /**
×
UNCOV
100
     * 文件字段
×
UNCOV
101
     */
×
UNCOV
102
    fileField?: string;
×
UNCOV
103

×
UNCOV
104
    /**
×
UNCOV
105
     * 文件名
×
UNCOV
106
     */
×
107
    fileName?: string;
108

109
    /**
×
110
     * 请求头
UNCOV
111
     */
×
112
    headers?: Record<string, string>;
113

UNCOV
114
    /**
×
UNCOV
115
     * 请求数据
×
UNCOV
116
     */
×
UNCOV
117
    data?: Record<string, string>;
×
UNCOV
118

×
119
    /**
120
     * 响应状态
121
     */
122
    responseStatus?: any;
UNCOV
123

×
124
    /**
125
     * 上传响应结果
126
     */
127
    response?: any;
128

129
    /**
UNCOV
130
     * 响应头
×
UNCOV
131
     */
×
UNCOV
132
    responseHeaders?: any;
×
UNCOV
133

×
UNCOV
134
    /**
×
UNCOV
135
     * 上传进度
×
UNCOV
136
     */
×
137
    progress?: ThyUploadFileProgress;
138
}
139

140
export interface ThyUploadFilesOptions {
141
    onStarted?: (item: ThyUploadFile) => void;
142
    onDone?: (item: ThyUploadFile) => void;
143
}
144

145
/**
146
 * 文件远程上传服务
147
 * @order 20
×
UNCOV
148
 */
×
UNCOV
149
@Injectable()
×
UNCOV
150
export class ThyUploadService {
×
UNCOV
151
    private xhrFactory = inject(XhrFactory);
×
UNCOV
152

×
153
    private secondsToHuman(sec: number): string {
UNCOV
154
        return new TinyDate(sec * 1000).nativeDate.toISOString().slice(11, 19);
×
UNCOV
155
    }
×
156

157
    private normalizeUploadFiles(uploadFiles: ThyUploadFile | ThyUploadFile[]) {
158
        coerceArray(uploadFiles).forEach(uploadFile => {
UNCOV
159
            if (!uploadFile.progress) {
×
160
                uploadFile.progress = {
UNCOV
161
                    status: ThyUploadStatus.pending,
×
162
                    percentage: 0,
163
                    startTime: 0
164
                };
1✔
165
            }
166
        });
167
    }
168

169
    private uploadByXhr(observer: Subscriber<ThyUploadResponse>, uploadFile: ThyUploadFile) {
170
        const xhr = this.xhrFactory.build();
171
        const time: number = new TinyDate().getTime();
172
        let speed = 0;
173
        let estimatedTime: number | null = null;
174

175
        uploadFile.progress = {
176
            status: ThyUploadStatus.started,
177
            percentage: 0,
178
            startTime: time
179
        };
180

181
        const onProgress = (event: ProgressEvent): void => {
182
            if (event.lengthComputable) {
183
                let percentage = Math.round((event.loaded * 100) / event.total);
184
                if (percentage === 100) {
185
                    percentage = 99;
186
                }
187
                const diff = new TinyDate().getTime() - time;
188
                speed = Math.round((event.loaded / diff) * 1000);
189
                const progressStartTime = (uploadFile.progress && uploadFile.progress.startTime) || new TinyDate().getTime();
190
                estimatedTime = Math.ceil((event.total - event.loaded) / speed);
191

192
                uploadFile.progress.status = ThyUploadStatus.uploading;
193
                uploadFile.progress.percentage = percentage;
194
                uploadFile.progress.speed = speed;
195
                uploadFile.progress.speedHuman = `${humanizeBytes(speed, false, 2)}/s`;
196
                uploadFile.progress.startTime = progressStartTime;
197
                uploadFile.progress.estimatedTime = estimatedTime;
198
                uploadFile.progress.estimatedTimeHuman = this.secondsToHuman(estimatedTime);
199

200
                observer.next({ status: ThyUploadStatus.uploading, uploadFile: uploadFile });
201
            }
202
        };
203

204
        const onError = (error: ErrorEvent) => observer.error(error);
205

206
        const onReadyStateChange = () => {
207
            if (xhr.readyState === XMLHttpRequest.DONE) {
208
                const speedTime = (new TinyDate().getTime() - uploadFile.progress.startTime) * 1000;
209
                const speedAverage = Math.round(uploadFile.nativeFile.size / speedTime);
210

211
                uploadFile.progress.status = ThyUploadStatus.done;
212
                uploadFile.progress.percentage = 100;
213
                uploadFile.progress.speed = speedAverage;
214
                uploadFile.progress.speedHuman = `${humanizeBytes(speed, false, 2)}/s`;
215
                uploadFile.progress.estimatedTime = estimatedTime;
216
                uploadFile.progress.estimatedTimeHuman = this.secondsToHuman(estimatedTime || 0);
217

218
                uploadFile.responseStatus = xhr.status;
219

220
                try {
221
                    uploadFile.response = JSON.parse(xhr.response);
222
                } catch (e) {
223
                    uploadFile.response = xhr.response;
224
                }
225

226
                // file.responseHeaders = this.parseResponseHeaders(xhr.getAllResponseHeaders());
227

228
                observer.next({ status: ThyUploadStatus.done, uploadFile: uploadFile });
229

230
                observer.complete();
231
            }
232
        };
233

234
        xhr.upload.addEventListener('progress', onProgress, false);
235
        xhr.upload.addEventListener('error', onError);
236
        // When using the [timeout attribute](https://xhr.spec.whatwg.org/#the-timeout-attribute) and an XHR
237
        // request times out, browsers trigger the `timeout` event (and executes the XHR's `ontimeout`
238
        // callback). Additionally, Safari 9 handles timed-out requests in the same way, even if no `timeout`
239
        // has been explicitly set on the XHR.
240
        xhr.upload.addEventListener('timeout', onError);
241
        xhr.addEventListener('timeout', onError);
242
        xhr.addEventListener('readystatechange', onReadyStateChange);
243

244
        xhr.open(uploadFile.method, uploadFile.url, true);
245
        xhr.withCredentials = uploadFile.withCredentials ? true : false;
246

247
        try {
248
            const formData = new FormData();
249

250
            Object.keys(uploadFile.data || {}).forEach(key => formData.append(key, uploadFile.data[key]));
251
            Object.keys(uploadFile.headers || {}).forEach(key => xhr.setRequestHeader(key, uploadFile.headers[key]));
252

253
            formData.append(uploadFile.fileField || 'file', uploadFile.nativeFile, uploadFile.fileName);
254

255
            observer.next({ status: ThyUploadStatus.started, uploadFile: uploadFile });
256
            xhr.send(formData);
257
        } catch (error) {
258
            observer.error(error);
259
        }
260

261
        return {
262
            xhr,
263
            cleanup: () => {
264
                xhr.upload.removeEventListener('progress', onProgress);
265
                xhr.upload.removeEventListener('error', onError);
266
                xhr.upload.removeEventListener('timeout', onError);
267
                xhr.removeEventListener('timeout', onError);
268
                xhr.removeEventListener('readystatechange', onReadyStateChange);
269
            }
270
        };
271
    }
272

273
    private ensureFileName(uploadFile: ThyUploadFile) {
274
        uploadFile.fileName = uploadFile.fileName || uploadFile.nativeFile.name;
275
    }
276

277
    /**
278
     * 上传单个文件
279
     * @param uploadFile 上传文件对象
280
     */
281
    upload(uploadFile: ThyUploadFile): Observable<ThyUploadResponse> {
282
        this.ensureFileName(uploadFile);
283

284
        return new Observable(observer => {
285
            const { xhr, cleanup } = this.uploadByXhr(observer, uploadFile);
286
            return () => {
287
                cleanup();
288

289
                if (xhr.readyState !== xhr.DONE) {
290
                    xhr.abort();
291
                }
292
            };
293
        });
294
    }
295

296
    /**
297
     * 并发上传多个文件
298
     * @param uploadFiles 上传文件列表
299
     * @param concurrent 并发上传数, 默认 5
300
     * @param options onStared, onDone 回调
301
     */
302
    uploadBulk(uploadFiles: ThyUploadFile[], concurrent = 5, options?: ThyUploadFilesOptions): Observable<ThyUploadResponse> {
303
        this.normalizeUploadFiles(uploadFiles);
304
        const result = from(uploadFiles).pipe(
305
            mergeMap(uploadFile => {
306
                return this.upload(uploadFile).pipe(
307
                    tap(uploadResponse => {
308
                        if (options && options.onStarted && uploadResponse.status === ThyUploadStatus.started) {
309
                            options.onStarted(uploadResponse.uploadFile);
310
                        }
311
                        if (options && options.onDone && uploadResponse.status === ThyUploadStatus.done) {
312
                            options.onDone(uploadResponse.uploadFile);
313
                        }
314
                    })
315
                );
316
            }, concurrent),
317
            map(response => {
318
                return response;
319
            })
320
        );
321

322
        return result;
323
    }
324
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc