@etsoo/shared 1.1.84 → 1.1.86
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -0
- package/__tests__/ContentDisposition.ts +24 -0
- package/lib/cjs/DomUtils.d.ts +8 -0
- package/lib/cjs/DomUtils.js +43 -0
- package/lib/cjs/index.d.ts +1 -0
- package/lib/cjs/index.js +1 -0
- package/lib/cjs/types/ContentDisposition.d.ts +22 -0
- package/lib/cjs/types/ContentDisposition.js +75 -0
- package/lib/mjs/DomUtils.d.ts +8 -0
- package/lib/mjs/DomUtils.js +43 -0
- package/lib/mjs/index.d.ts +1 -0
- package/lib/mjs/index.js +1 -0
- package/lib/mjs/types/ContentDisposition.d.ts +22 -0
- package/lib/mjs/types/ContentDisposition.js +71 -0
- package/package.json +2 -7
- package/src/DomUtils.ts +48 -0
- package/src/index.ts +1 -0
- package/src/types/ContentDisposition.ts +81 -0
package/README.md
CHANGED
|
@@ -32,6 +32,15 @@ $ yarn add @etsoo/shared
|
|
|
32
32
|
## storage
|
|
33
33
|
Storage interface and browser storage implementation
|
|
34
34
|
|
|
35
|
+
## ContentDisposition
|
|
36
|
+
Content disposition of HTTP
|
|
37
|
+
|
|
38
|
+
|Name|Description|
|
|
39
|
+
|---:|---|
|
|
40
|
+
|static parse|Parse header value|
|
|
41
|
+
|Methods||
|
|
42
|
+
|format|Format to standard output|
|
|
43
|
+
|
|
35
44
|
## EColor
|
|
36
45
|
Etsoo implmented Color
|
|
37
46
|
|
|
@@ -182,6 +191,7 @@ DOM/window related utilities
|
|
|
182
191
|
|detectedCountry|Current detected country|
|
|
183
192
|
|detectedCulture|Current detected culture|
|
|
184
193
|
|dimensionEqual|Check two rectangles equality|
|
|
194
|
+
|downloadFile|Download file from API fetch response body|
|
|
185
195
|
|fileToDataURL|File to data URL|
|
|
186
196
|
|formDataToObject|Form data to object|
|
|
187
197
|
|getCulture|Get the available culture definition|
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { ContentDisposition } from '../src/types/ContentDisposition';
|
|
2
|
+
|
|
3
|
+
test('Tests for parse', () => {
|
|
4
|
+
const cd1 = ContentDisposition.parse(
|
|
5
|
+
"attachment; filename=__PDF.pdf; filename*=UTF-8''%E6%B5%8B%E8%AF%95PDF.pdf"
|
|
6
|
+
);
|
|
7
|
+
|
|
8
|
+
const cd2 = ContentDisposition.parse(
|
|
9
|
+
`attachment; filename="__PDF.pdf"; filename*="UTF-8''%E6%B5%8B%E8%AF%95PDF.pdf"`
|
|
10
|
+
);
|
|
11
|
+
|
|
12
|
+
expect(cd1?.type).toBe(cd2?.type);
|
|
13
|
+
expect(cd1?.filename).toBe(cd2?.filename);
|
|
14
|
+
expect(cd1?.filename).toBe('测试PDF.pdf');
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
test('Tests for format', () => {
|
|
18
|
+
const cd1 = new ContentDisposition('form-data', 'a-b.jpg', 'file');
|
|
19
|
+
expect(cd1.format()).toBe(`form-data; name="file"; filename="a-b.jpg"`);
|
|
20
|
+
const cd2 = new ContentDisposition('attachment', '测试PDF.pdf');
|
|
21
|
+
expect(cd2.format()).toBe(
|
|
22
|
+
`attachment; filename="__PDF.pdf"; filename*="UTF-8''%E6%B5%8B%E8%AF%95PDF.pdf"`
|
|
23
|
+
);
|
|
24
|
+
});
|
package/lib/cjs/DomUtils.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
/// <reference lib="dom" />
|
|
1
2
|
import { DataTypes } from './DataTypes';
|
|
2
3
|
import { FormDataFieldValue, IFormData } from './types/FormData';
|
|
3
4
|
/**
|
|
@@ -50,6 +51,13 @@ export declare namespace DomUtils {
|
|
|
50
51
|
* @param d2 Dimension 2
|
|
51
52
|
*/
|
|
52
53
|
function dimensionEqual(d1?: DOMRect, d2?: DOMRect): boolean;
|
|
54
|
+
/**
|
|
55
|
+
* Download file from API fetch response body
|
|
56
|
+
* @param data Data
|
|
57
|
+
* @param suggestedName Suggested file name
|
|
58
|
+
* @param autoDetect Auto detect, false will use link click way
|
|
59
|
+
*/
|
|
60
|
+
function downloadFile(data: ReadableStream | Blob, suggestedName?: string, autoDetect?: boolean): Promise<void>;
|
|
53
61
|
/**
|
|
54
62
|
* File to data URL
|
|
55
63
|
* @param file File
|
package/lib/cjs/DomUtils.js
CHANGED
|
@@ -10,6 +10,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
|
|
|
10
10
|
};
|
|
11
11
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
12
|
exports.DomUtils = void 0;
|
|
13
|
+
/// <reference lib="dom" />
|
|
13
14
|
const DataTypes_1 = require("./DataTypes");
|
|
14
15
|
if (typeof navigator === 'undefined') {
|
|
15
16
|
// Test mock only
|
|
@@ -231,6 +232,48 @@ var DomUtils;
|
|
|
231
232
|
return false;
|
|
232
233
|
}
|
|
233
234
|
DomUtils.dimensionEqual = dimensionEqual;
|
|
235
|
+
/**
|
|
236
|
+
* Download file from API fetch response body
|
|
237
|
+
* @param data Data
|
|
238
|
+
* @param suggestedName Suggested file name
|
|
239
|
+
* @param autoDetect Auto detect, false will use link click way
|
|
240
|
+
*/
|
|
241
|
+
function downloadFile(data, suggestedName, autoDetect = true) {
|
|
242
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
243
|
+
try {
|
|
244
|
+
if (autoDetect && 'showSaveFilePicker' in globalThis) {
|
|
245
|
+
const handle = yield globalThis.showSaveFilePicker({
|
|
246
|
+
suggestedName
|
|
247
|
+
});
|
|
248
|
+
const stream = yield handle.createWritable();
|
|
249
|
+
if (data instanceof Blob) {
|
|
250
|
+
data.stream().pipeTo(stream);
|
|
251
|
+
}
|
|
252
|
+
else {
|
|
253
|
+
yield data.pipeTo(stream);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
else {
|
|
257
|
+
const url = window.URL.createObjectURL(data instanceof Blob
|
|
258
|
+
? data
|
|
259
|
+
: yield new Response(data).blob());
|
|
260
|
+
const a = document.createElement('a');
|
|
261
|
+
a.style.display = 'none';
|
|
262
|
+
a.href = url;
|
|
263
|
+
if (suggestedName)
|
|
264
|
+
a.download = suggestedName;
|
|
265
|
+
document.body.appendChild(a);
|
|
266
|
+
a.click();
|
|
267
|
+
a.remove();
|
|
268
|
+
window.URL.revokeObjectURL(url);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
catch (e) {
|
|
272
|
+
console.log(e);
|
|
273
|
+
}
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
DomUtils.downloadFile = downloadFile;
|
|
234
277
|
/**
|
|
235
278
|
* File to data URL
|
|
236
279
|
* @param file File
|
package/lib/cjs/index.d.ts
CHANGED
package/lib/cjs/index.js
CHANGED
|
@@ -14,6 +14,7 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
|
14
14
|
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
15
|
};
|
|
16
16
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
__exportStar(require("./types/ContentDisposition"), exports);
|
|
17
18
|
__exportStar(require("./types/DelayedExecutorType"), exports);
|
|
18
19
|
__exportStar(require("./types/EColor"), exports);
|
|
19
20
|
__exportStar(require("./types/EHistory"), exports);
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Content disposition of HTTP
|
|
3
|
+
* https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Disposition
|
|
4
|
+
*/
|
|
5
|
+
export declare class ContentDisposition {
|
|
6
|
+
readonly type: string;
|
|
7
|
+
readonly filename: string;
|
|
8
|
+
readonly name?: string | undefined;
|
|
9
|
+
constructor(type: 'inline' | 'attachment', filename: string);
|
|
10
|
+
constructor(type: 'form-data', filename: string, name: string);
|
|
11
|
+
/**
|
|
12
|
+
* Format to standard output
|
|
13
|
+
* @returns Result
|
|
14
|
+
*/
|
|
15
|
+
format(): string;
|
|
16
|
+
/**
|
|
17
|
+
* Parse header value
|
|
18
|
+
* @param header Content-Disposition header value
|
|
19
|
+
* @returns Object
|
|
20
|
+
*/
|
|
21
|
+
static parse(header: string | undefined | null): ContentDisposition | undefined;
|
|
22
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ContentDisposition = void 0;
|
|
4
|
+
const Utils_1 = require("../Utils");
|
|
5
|
+
/**
|
|
6
|
+
* Content disposition of HTTP
|
|
7
|
+
* https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Disposition
|
|
8
|
+
*/
|
|
9
|
+
class ContentDisposition {
|
|
10
|
+
constructor(type, filename, name) {
|
|
11
|
+
this.type = type;
|
|
12
|
+
this.filename = filename;
|
|
13
|
+
this.name = name;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Format to standard output
|
|
17
|
+
* @returns Result
|
|
18
|
+
*/
|
|
19
|
+
format() {
|
|
20
|
+
const items = [this.type];
|
|
21
|
+
if (this.name)
|
|
22
|
+
items.push(`name="${this.name}"`);
|
|
23
|
+
const filename1 = this.filename.replace(/[^a-zA-Z0-9\.-]/g, '_');
|
|
24
|
+
items.push(`filename="${filename1}"`);
|
|
25
|
+
if (filename1 != this.filename)
|
|
26
|
+
items.push(`filename*="UTF-8''${encodeURIComponent(this.filename)}"`);
|
|
27
|
+
return items.join('; ');
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Parse header value
|
|
31
|
+
* @param header Content-Disposition header value
|
|
32
|
+
* @returns Object
|
|
33
|
+
*/
|
|
34
|
+
static parse(header) {
|
|
35
|
+
if (!header)
|
|
36
|
+
return undefined;
|
|
37
|
+
const parts = header.trim().split(/\s*;\s*/g);
|
|
38
|
+
const len = parts.length;
|
|
39
|
+
if (len < 2)
|
|
40
|
+
return undefined;
|
|
41
|
+
const type = parts[0];
|
|
42
|
+
let name;
|
|
43
|
+
let filename = '';
|
|
44
|
+
for (let i = 1; i < len; i++) {
|
|
45
|
+
const part = parts[i];
|
|
46
|
+
let [field, value] = part.split(/\s*=\s*/g);
|
|
47
|
+
// case-insensitive
|
|
48
|
+
field = field.toLowerCase();
|
|
49
|
+
// Remove quotes
|
|
50
|
+
value = Utils_1.Utils.trim(value, '"');
|
|
51
|
+
if (field === 'name') {
|
|
52
|
+
name = value;
|
|
53
|
+
}
|
|
54
|
+
else if (field === 'filename') {
|
|
55
|
+
if (filename === '')
|
|
56
|
+
filename = value;
|
|
57
|
+
}
|
|
58
|
+
else if (field === 'filename*') {
|
|
59
|
+
const pos = value.indexOf("''");
|
|
60
|
+
filename =
|
|
61
|
+
pos == -1
|
|
62
|
+
? value
|
|
63
|
+
: decodeURIComponent(value.substring(pos + 2));
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
if (type === 'form-data') {
|
|
67
|
+
return new ContentDisposition(type, filename, name !== null && name !== void 0 ? name : 'file');
|
|
68
|
+
}
|
|
69
|
+
if (type === 'inline' || type === 'attachment') {
|
|
70
|
+
return new ContentDisposition(type, filename);
|
|
71
|
+
}
|
|
72
|
+
return undefined;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
exports.ContentDisposition = ContentDisposition;
|
package/lib/mjs/DomUtils.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
/// <reference lib="dom" />
|
|
1
2
|
import { DataTypes } from './DataTypes';
|
|
2
3
|
import { FormDataFieldValue, IFormData } from './types/FormData';
|
|
3
4
|
/**
|
|
@@ -50,6 +51,13 @@ export declare namespace DomUtils {
|
|
|
50
51
|
* @param d2 Dimension 2
|
|
51
52
|
*/
|
|
52
53
|
function dimensionEqual(d1?: DOMRect, d2?: DOMRect): boolean;
|
|
54
|
+
/**
|
|
55
|
+
* Download file from API fetch response body
|
|
56
|
+
* @param data Data
|
|
57
|
+
* @param suggestedName Suggested file name
|
|
58
|
+
* @param autoDetect Auto detect, false will use link click way
|
|
59
|
+
*/
|
|
60
|
+
function downloadFile(data: ReadableStream | Blob, suggestedName?: string, autoDetect?: boolean): Promise<void>;
|
|
53
61
|
/**
|
|
54
62
|
* File to data URL
|
|
55
63
|
* @param file File
|
package/lib/mjs/DomUtils.js
CHANGED
|
@@ -7,6 +7,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
|
|
|
7
7
|
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
8
8
|
});
|
|
9
9
|
};
|
|
10
|
+
/// <reference lib="dom" />
|
|
10
11
|
import { DataTypes } from './DataTypes';
|
|
11
12
|
if (typeof navigator === 'undefined') {
|
|
12
13
|
// Test mock only
|
|
@@ -228,6 +229,48 @@ export var DomUtils;
|
|
|
228
229
|
return false;
|
|
229
230
|
}
|
|
230
231
|
DomUtils.dimensionEqual = dimensionEqual;
|
|
232
|
+
/**
|
|
233
|
+
* Download file from API fetch response body
|
|
234
|
+
* @param data Data
|
|
235
|
+
* @param suggestedName Suggested file name
|
|
236
|
+
* @param autoDetect Auto detect, false will use link click way
|
|
237
|
+
*/
|
|
238
|
+
function downloadFile(data, suggestedName, autoDetect = true) {
|
|
239
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
240
|
+
try {
|
|
241
|
+
if (autoDetect && 'showSaveFilePicker' in globalThis) {
|
|
242
|
+
const handle = yield globalThis.showSaveFilePicker({
|
|
243
|
+
suggestedName
|
|
244
|
+
});
|
|
245
|
+
const stream = yield handle.createWritable();
|
|
246
|
+
if (data instanceof Blob) {
|
|
247
|
+
data.stream().pipeTo(stream);
|
|
248
|
+
}
|
|
249
|
+
else {
|
|
250
|
+
yield data.pipeTo(stream);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
else {
|
|
254
|
+
const url = window.URL.createObjectURL(data instanceof Blob
|
|
255
|
+
? data
|
|
256
|
+
: yield new Response(data).blob());
|
|
257
|
+
const a = document.createElement('a');
|
|
258
|
+
a.style.display = 'none';
|
|
259
|
+
a.href = url;
|
|
260
|
+
if (suggestedName)
|
|
261
|
+
a.download = suggestedName;
|
|
262
|
+
document.body.appendChild(a);
|
|
263
|
+
a.click();
|
|
264
|
+
a.remove();
|
|
265
|
+
window.URL.revokeObjectURL(url);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
catch (e) {
|
|
269
|
+
console.log(e);
|
|
270
|
+
}
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
DomUtils.downloadFile = downloadFile;
|
|
231
274
|
/**
|
|
232
275
|
* File to data URL
|
|
233
276
|
* @param file File
|
package/lib/mjs/index.d.ts
CHANGED
package/lib/mjs/index.js
CHANGED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Content disposition of HTTP
|
|
3
|
+
* https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Disposition
|
|
4
|
+
*/
|
|
5
|
+
export declare class ContentDisposition {
|
|
6
|
+
readonly type: string;
|
|
7
|
+
readonly filename: string;
|
|
8
|
+
readonly name?: string | undefined;
|
|
9
|
+
constructor(type: 'inline' | 'attachment', filename: string);
|
|
10
|
+
constructor(type: 'form-data', filename: string, name: string);
|
|
11
|
+
/**
|
|
12
|
+
* Format to standard output
|
|
13
|
+
* @returns Result
|
|
14
|
+
*/
|
|
15
|
+
format(): string;
|
|
16
|
+
/**
|
|
17
|
+
* Parse header value
|
|
18
|
+
* @param header Content-Disposition header value
|
|
19
|
+
* @returns Object
|
|
20
|
+
*/
|
|
21
|
+
static parse(header: string | undefined | null): ContentDisposition | undefined;
|
|
22
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { Utils } from '../Utils';
|
|
2
|
+
/**
|
|
3
|
+
* Content disposition of HTTP
|
|
4
|
+
* https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Disposition
|
|
5
|
+
*/
|
|
6
|
+
export class ContentDisposition {
|
|
7
|
+
constructor(type, filename, name) {
|
|
8
|
+
this.type = type;
|
|
9
|
+
this.filename = filename;
|
|
10
|
+
this.name = name;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Format to standard output
|
|
14
|
+
* @returns Result
|
|
15
|
+
*/
|
|
16
|
+
format() {
|
|
17
|
+
const items = [this.type];
|
|
18
|
+
if (this.name)
|
|
19
|
+
items.push(`name="${this.name}"`);
|
|
20
|
+
const filename1 = this.filename.replace(/[^a-zA-Z0-9\.-]/g, '_');
|
|
21
|
+
items.push(`filename="${filename1}"`);
|
|
22
|
+
if (filename1 != this.filename)
|
|
23
|
+
items.push(`filename*="UTF-8''${encodeURIComponent(this.filename)}"`);
|
|
24
|
+
return items.join('; ');
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Parse header value
|
|
28
|
+
* @param header Content-Disposition header value
|
|
29
|
+
* @returns Object
|
|
30
|
+
*/
|
|
31
|
+
static parse(header) {
|
|
32
|
+
if (!header)
|
|
33
|
+
return undefined;
|
|
34
|
+
const parts = header.trim().split(/\s*;\s*/g);
|
|
35
|
+
const len = parts.length;
|
|
36
|
+
if (len < 2)
|
|
37
|
+
return undefined;
|
|
38
|
+
const type = parts[0];
|
|
39
|
+
let name;
|
|
40
|
+
let filename = '';
|
|
41
|
+
for (let i = 1; i < len; i++) {
|
|
42
|
+
const part = parts[i];
|
|
43
|
+
let [field, value] = part.split(/\s*=\s*/g);
|
|
44
|
+
// case-insensitive
|
|
45
|
+
field = field.toLowerCase();
|
|
46
|
+
// Remove quotes
|
|
47
|
+
value = Utils.trim(value, '"');
|
|
48
|
+
if (field === 'name') {
|
|
49
|
+
name = value;
|
|
50
|
+
}
|
|
51
|
+
else if (field === 'filename') {
|
|
52
|
+
if (filename === '')
|
|
53
|
+
filename = value;
|
|
54
|
+
}
|
|
55
|
+
else if (field === 'filename*') {
|
|
56
|
+
const pos = value.indexOf("''");
|
|
57
|
+
filename =
|
|
58
|
+
pos == -1
|
|
59
|
+
? value
|
|
60
|
+
: decodeURIComponent(value.substring(pos + 2));
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
if (type === 'form-data') {
|
|
64
|
+
return new ContentDisposition(type, filename, name !== null && name !== void 0 ? name : 'file');
|
|
65
|
+
}
|
|
66
|
+
if (type === 'inline' || type === 'attachment') {
|
|
67
|
+
return new ContentDisposition(type, filename);
|
|
68
|
+
}
|
|
69
|
+
return undefined;
|
|
70
|
+
}
|
|
71
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@etsoo/shared",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.86",
|
|
4
4
|
"description": "TypeScript shared utilities and functions",
|
|
5
5
|
"main": "lib/cjs/index.js",
|
|
6
6
|
"module": "lib/mjs/index.js",
|
|
@@ -54,13 +54,8 @@
|
|
|
54
54
|
},
|
|
55
55
|
"homepage": "https://github.com/ETSOO/Shared#readme",
|
|
56
56
|
"devDependencies": {
|
|
57
|
-
"@types/jest": "^29.2.
|
|
57
|
+
"@types/jest": "^29.2.5",
|
|
58
58
|
"@types/lodash.isequal": "^4.5.6",
|
|
59
|
-
"@typescript-eslint/eslint-plugin": "^5.47.0",
|
|
60
|
-
"@typescript-eslint/parser": "^5.47.0",
|
|
61
|
-
"eslint": "^8.30.0",
|
|
62
|
-
"eslint-config-airbnb-base": "^15.0.0",
|
|
63
|
-
"eslint-plugin-import": "^2.26.0",
|
|
64
59
|
"jest": "^29.3.1",
|
|
65
60
|
"jest-environment-jsdom": "^29.3.1",
|
|
66
61
|
"ts-jest": "^29.0.3",
|
package/src/DomUtils.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
/// <reference lib="dom" />
|
|
1
2
|
import { DataTypes } from './DataTypes';
|
|
2
3
|
import { FormDataFieldValue, IFormData } from './types/FormData';
|
|
3
4
|
|
|
@@ -270,6 +271,53 @@ export namespace DomUtils {
|
|
|
270
271
|
return false;
|
|
271
272
|
}
|
|
272
273
|
|
|
274
|
+
/**
|
|
275
|
+
* Download file from API fetch response body
|
|
276
|
+
* @param data Data
|
|
277
|
+
* @param suggestedName Suggested file name
|
|
278
|
+
* @param autoDetect Auto detect, false will use link click way
|
|
279
|
+
*/
|
|
280
|
+
export async function downloadFile(
|
|
281
|
+
data: ReadableStream | Blob,
|
|
282
|
+
suggestedName?: string,
|
|
283
|
+
autoDetect: boolean = true
|
|
284
|
+
) {
|
|
285
|
+
try {
|
|
286
|
+
if (autoDetect && 'showSaveFilePicker' in globalThis) {
|
|
287
|
+
const handle = await (globalThis as any).showSaveFilePicker({
|
|
288
|
+
suggestedName
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
const stream = await handle.createWritable();
|
|
292
|
+
|
|
293
|
+
if (data instanceof Blob) {
|
|
294
|
+
data.stream().pipeTo(stream);
|
|
295
|
+
} else {
|
|
296
|
+
await data.pipeTo(stream);
|
|
297
|
+
}
|
|
298
|
+
} else {
|
|
299
|
+
const url = window.URL.createObjectURL(
|
|
300
|
+
data instanceof Blob
|
|
301
|
+
? data
|
|
302
|
+
: await new Response(data).blob()
|
|
303
|
+
);
|
|
304
|
+
|
|
305
|
+
const a = document.createElement('a');
|
|
306
|
+
a.style.display = 'none';
|
|
307
|
+
a.href = url;
|
|
308
|
+
if (suggestedName) a.download = suggestedName;
|
|
309
|
+
|
|
310
|
+
document.body.appendChild(a);
|
|
311
|
+
a.click();
|
|
312
|
+
a.remove();
|
|
313
|
+
|
|
314
|
+
window.URL.revokeObjectURL(url);
|
|
315
|
+
}
|
|
316
|
+
} catch (e) {
|
|
317
|
+
console.log(e);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
273
321
|
/**
|
|
274
322
|
* File to data URL
|
|
275
323
|
* @param file File
|
package/src/index.ts
CHANGED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { Utils } from '../Utils';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Content disposition of HTTP
|
|
5
|
+
* https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Disposition
|
|
6
|
+
*/
|
|
7
|
+
export class ContentDisposition {
|
|
8
|
+
constructor(type: 'inline' | 'attachment', filename: string);
|
|
9
|
+
constructor(type: 'form-data', filename: string, name: string);
|
|
10
|
+
constructor(
|
|
11
|
+
public readonly type: string,
|
|
12
|
+
public readonly filename: string,
|
|
13
|
+
public readonly name?: string
|
|
14
|
+
) {}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Format to standard output
|
|
18
|
+
* @returns Result
|
|
19
|
+
*/
|
|
20
|
+
format() {
|
|
21
|
+
const items = [this.type];
|
|
22
|
+
if (this.name) items.push(`name="${this.name}"`);
|
|
23
|
+
const filename1 = this.filename.replace(/[^a-zA-Z0-9\.-]/g, '_');
|
|
24
|
+
items.push(`filename="${filename1}"`);
|
|
25
|
+
if (filename1 != this.filename)
|
|
26
|
+
items.push(
|
|
27
|
+
`filename*="UTF-8''${encodeURIComponent(this.filename)}"`
|
|
28
|
+
);
|
|
29
|
+
return items.join('; ');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Parse header value
|
|
34
|
+
* @param header Content-Disposition header value
|
|
35
|
+
* @returns Object
|
|
36
|
+
*/
|
|
37
|
+
static parse(header: string | undefined | null) {
|
|
38
|
+
if (!header) return undefined;
|
|
39
|
+
|
|
40
|
+
const parts = header.trim().split(/\s*;\s*/g);
|
|
41
|
+
const len = parts.length;
|
|
42
|
+
if (len < 2) return undefined;
|
|
43
|
+
|
|
44
|
+
const type = parts[0];
|
|
45
|
+
let name: string | undefined;
|
|
46
|
+
let filename: string = '';
|
|
47
|
+
|
|
48
|
+
for (let i = 1; i < len; i++) {
|
|
49
|
+
const part = parts[i];
|
|
50
|
+
let [field, value] = part.split(/\s*=\s*/g);
|
|
51
|
+
|
|
52
|
+
// case-insensitive
|
|
53
|
+
field = field.toLowerCase();
|
|
54
|
+
|
|
55
|
+
// Remove quotes
|
|
56
|
+
value = Utils.trim(value, '"');
|
|
57
|
+
|
|
58
|
+
if (field === 'name') {
|
|
59
|
+
name = value;
|
|
60
|
+
} else if (field === 'filename') {
|
|
61
|
+
if (filename === '') filename = value;
|
|
62
|
+
} else if (field === 'filename*') {
|
|
63
|
+
const pos = value.indexOf("''");
|
|
64
|
+
filename =
|
|
65
|
+
pos == -1
|
|
66
|
+
? value
|
|
67
|
+
: decodeURIComponent(value.substring(pos + 2));
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (type === 'form-data') {
|
|
72
|
+
return new ContentDisposition(type, filename, name ?? 'file');
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (type === 'inline' || type === 'attachment') {
|
|
76
|
+
return new ContentDisposition(type, filename);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return undefined;
|
|
80
|
+
}
|
|
81
|
+
}
|