magic-builder 0.1.0

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.
@@ -0,0 +1,121 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const crypto = require('crypto');
5
+ const { request } = require('./http');
6
+
7
+ const PART_SIZE = 10 * 1024 * 1024; // 10MB
8
+ const MULTIPART_THRESHOLD = 16 * 1024 * 1024; // 16MB
9
+
10
+ function shouldUseMultipart(fileSize) {
11
+ return fileSize > MULTIPART_THRESHOLD;
12
+ }
13
+
14
+ async function uploadSingle(filePath, opts = {}) {
15
+ const { baseUrl, key, contentType, quiet } = opts;
16
+ const filename = require('path').basename(filePath);
17
+ const mime = contentType || require('./mime').getMimeType(filePath);
18
+
19
+ const signBody = { filename, contentType: mime };
20
+ if (key) signBody.key = key;
21
+
22
+ const signRes = await request(`${baseUrl}/api/tos/sign`, {
23
+ method: 'POST',
24
+ body: signBody,
25
+ });
26
+
27
+ if (signRes.code !== 0) throw new Error(signRes.msg || 'Sign failed');
28
+ const { signed_url, url } = signRes.data;
29
+
30
+ const buf = fs.readFileSync(filePath);
31
+ if (!quiet) process.stderr.write('Uploading... ');
32
+
33
+ await request(signed_url, {
34
+ method: 'PUT',
35
+ headers: { 'Content-Type': mime },
36
+ body: buf,
37
+ timeout: 120000,
38
+ });
39
+
40
+ if (!quiet) process.stderr.write('done\n');
41
+ return { url, key: signRes.data.key, size: buf.length };
42
+ }
43
+
44
+ async function uploadMultipart(filePath, opts = {}) {
45
+ const { baseUrl, key, contentType, quiet } = opts;
46
+ const filename = require('path').basename(filePath);
47
+ const mime = contentType || require('./mime').getMimeType(filePath);
48
+ const stat = fs.statSync(filePath);
49
+
50
+ const initBody = { filename, contentType: mime };
51
+ if (key) initBody.key = key;
52
+
53
+ const initRes = await request(`${baseUrl}/api/tos/multipart/init`, {
54
+ method: 'POST',
55
+ body: initBody,
56
+ });
57
+ if (initRes.code !== 0) throw new Error(initRes.msg || 'Multipart init failed');
58
+ const { uploadId, key: tosKey, url } = initRes.data;
59
+
60
+ const fd = fs.openSync(filePath, 'r');
61
+ const totalParts = Math.ceil(stat.size / PART_SIZE);
62
+ const parts = [];
63
+
64
+ try {
65
+ for (let i = 0; i < totalParts; i++) {
66
+ const start = i * PART_SIZE;
67
+ const end = Math.min(start + PART_SIZE, stat.size);
68
+ const buf = Buffer.alloc(end - start);
69
+ fs.readSync(fd, buf, 0, buf.length, start);
70
+
71
+ if (!quiet) process.stderr.write(`\rUploading part ${i + 1}/${totalParts}...`);
72
+
73
+ const boundary = crypto.randomBytes(16).toString('hex');
74
+ const partNum = i + 1;
75
+ const header = `--${boundary}\r\nContent-Disposition: form-data; name="file"; filename="${filename}"\r\nContent-Type: ${mime}\r\n\r\n`;
76
+ const footer = `\r\n--${boundary}--\r\n`;
77
+ const multipartBody = Buffer.concat([Buffer.from(header), buf, Buffer.from(footer)]);
78
+
79
+ const partRes = await request(`${baseUrl}/api/tos/multipart/part`, {
80
+ method: 'POST',
81
+ headers: {
82
+ 'Content-Type': `multipart/form-data; boundary=${boundary}`,
83
+ 'X-Upload-Id': uploadId,
84
+ 'X-Part-Number': String(partNum),
85
+ 'X-Key': tosKey,
86
+ },
87
+ body: multipartBody,
88
+ timeout: 300000,
89
+ });
90
+
91
+ if (partRes.code !== 0) {
92
+ await abortMultipart(baseUrl, uploadId, tosKey);
93
+ throw new Error(partRes.msg || `Part ${partNum} upload failed`);
94
+ }
95
+ parts.push({ partNumber: partNum, etag: partRes.data.etag });
96
+ }
97
+ } finally {
98
+ fs.closeSync(fd);
99
+ }
100
+
101
+ const completeRes = await request(`${baseUrl}/api/tos/multipart/complete`, {
102
+ method: 'POST',
103
+ body: { uploadId, key: tosKey, parts },
104
+ });
105
+
106
+ if (completeRes.code !== 0) throw new Error(completeRes.msg || 'Multipart complete failed');
107
+ if (!quiet) process.stderr.write('\rUpload complete. \n');
108
+
109
+ return { url: completeRes.data?.url || url, key: tosKey, size: stat.size };
110
+ }
111
+
112
+ async function abortMultipart(baseUrl, uploadId, key) {
113
+ try {
114
+ await request(`${baseUrl}/api/tos/multipart/abort`, {
115
+ method: 'POST',
116
+ body: { uploadId, key },
117
+ });
118
+ } catch (_) {}
119
+ }
120
+
121
+ module.exports = { uploadSingle, uploadMultipart, shouldUseMultipart, PART_SIZE, MULTIPART_THRESHOLD };
@@ -0,0 +1,40 @@
1
+ 'use strict';
2
+
3
+ const EXIT = { OK: 0, ERROR: 1, AUTH: 2, NETWORK: 3 };
4
+
5
+ function formatOutput(data, format = 'json') {
6
+ if (format === 'plain') {
7
+ if (typeof data === 'string') return data;
8
+ return Object.entries(data).map(([k, v]) => `${k}: ${v}`).join('\n');
9
+ }
10
+ if (format === 'table') {
11
+ if (typeof data === 'string') return data;
12
+ const keys = Object.keys(data);
13
+ const maxKey = Math.max(...keys.map(k => k.length));
14
+ return keys.map(k => `${k.padEnd(maxKey)} ${data[k]}`).join('\n');
15
+ }
16
+ return JSON.stringify(data, null, 2);
17
+ }
18
+
19
+ function success(data, opts = {}) {
20
+ process.stdout.write(formatOutput(data, opts.format) + '\n');
21
+ process.exit(EXIT.OK);
22
+ }
23
+
24
+ function fail(message, code = 'E_ERROR', exitCode = EXIT.ERROR) {
25
+ const out = JSON.stringify({ error: code, message });
26
+ process.stderr.write(out + '\n');
27
+ process.exit(exitCode);
28
+ }
29
+
30
+ function failFromHttpError(err) {
31
+ if (err.code === 'E_AUTH_FAILED' || err.code === 'E_NO_TOKEN') {
32
+ fail(err.message, err.code, EXIT.AUTH);
33
+ } else if (err.code === 'E_NETWORK') {
34
+ fail(err.message, err.code, EXIT.NETWORK);
35
+ } else {
36
+ fail(err.message, err.code || 'E_ERROR', EXIT.ERROR);
37
+ }
38
+ }
39
+
40
+ module.exports = { EXIT, formatOutput, success, fail, failFromHttpError };