@editframe/cli 0.8.0-beta.1 ā 0.8.0-beta.10
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/dist/VERSION.d.ts +1 -1
- package/dist/VERSION.js +1 -1
- package/dist/commands/process-file.js +6 -3
- package/dist/commands/render.js +3 -1
- package/dist/commands/webhook.d.ts +7 -0
- package/dist/commands/webhook.js +61 -0
- package/dist/index.d.ts +0 -1
- package/dist/index.js +1 -0
- package/dist/operations/processRenderInfo.d.ts +0 -1
- package/dist/operations/syncAssetsDirectory.js +116 -64
- package/dist/utils/attachWorkbench.d.ts +0 -1
- package/dist/utils/getFolderSize.d.ts +1 -0
- package/dist/utils/getFolderSize.js +22 -0
- package/dist/utils/index.d.ts +0 -1
- package/dist/utils/launchBrowserAndWaitForSDK.d.ts +0 -1
- package/dist/utils/startDevServer.d.ts +0 -1
- package/dist/utils/startPreviewServer.d.ts +0 -1
- package/dist/utils/validateVideoResolution.d.ts +1 -1
- package/package.json +7 -6
- package/src/commands/process-file.ts +6 -2
- package/src/commands/render.ts +3 -1
- package/src/commands/webhook.ts +76 -0
- package/src/operations/syncAssetsDirectory.ts +122 -49
- package/src/utils/getFolderSize.ts +24 -0
package/dist/VERSION.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const VERSION = "0.8.0-beta.
|
|
1
|
+
export declare const VERSION = "0.8.0-beta.10";
|
package/dist/VERSION.js
CHANGED
|
@@ -1,23 +1,26 @@
|
|
|
1
1
|
import { basename } from "node:path";
|
|
2
|
+
import { stat } from "node:fs/promises";
|
|
3
|
+
import { createReadStream } from "node:fs";
|
|
2
4
|
import { program } from "commander";
|
|
3
5
|
import { withSpinner } from "../utils/withSpinner.js";
|
|
4
6
|
import { getClient } from "../utils/index.js";
|
|
5
|
-
import { createReadStream } from "node:fs";
|
|
6
7
|
import { createUnprocessedFile, uploadUnprocessedFile, updateUnprocessedFile } from "@editframe/api";
|
|
7
8
|
import { md5FilePath } from "@editframe/assets";
|
|
8
9
|
program.command("process-file <file>").description("Upload a audio/video to Editframe for processing.").action(async (path) => {
|
|
9
10
|
const client = getClient();
|
|
10
11
|
const fileId = await md5FilePath(path);
|
|
12
|
+
const byte_size = (await stat(path)).size;
|
|
11
13
|
await withSpinner("Creating unprocessed file record", async () => {
|
|
12
14
|
await createUnprocessedFile(client, {
|
|
13
15
|
id: fileId,
|
|
14
16
|
processes: [],
|
|
15
|
-
filename: basename(path)
|
|
17
|
+
filename: basename(path),
|
|
18
|
+
byte_size
|
|
16
19
|
});
|
|
17
20
|
});
|
|
18
21
|
const readStream = createReadStream(path);
|
|
19
22
|
await withSpinner("Uploading file", async () => {
|
|
20
|
-
await uploadUnprocessedFile(client, fileId, readStream);
|
|
23
|
+
await uploadUnprocessedFile(client, fileId, readStream, byte_size);
|
|
21
24
|
});
|
|
22
25
|
const unprocessedFile = await withSpinner(
|
|
23
26
|
"Marking for processing",
|
package/dist/commands/render.js
CHANGED
|
@@ -16,6 +16,7 @@ import { getClient } from "../utils/index.js";
|
|
|
16
16
|
import { getRenderInfo } from "../operations/getRenderInfo.js";
|
|
17
17
|
import { processRenderInfo } from "../operations/processRenderInfo.js";
|
|
18
18
|
import { validateVideoResolution } from "../utils/validateVideoResolution.js";
|
|
19
|
+
import { getFolderSize } from "../utils/getFolderSize.js";
|
|
19
20
|
const buildProductionUrl = async (origin, tagName, assetPath) => {
|
|
20
21
|
const md5Sum = await md5FilePath(assetPath);
|
|
21
22
|
const basename = path.basename(assetPath);
|
|
@@ -142,7 +143,8 @@ program.command("render [directory]").description(
|
|
|
142
143
|
);
|
|
143
144
|
const readable = new PassThrough();
|
|
144
145
|
tarStream.pipe(readable);
|
|
145
|
-
await
|
|
146
|
+
const folderSize = await getFolderSize(distDir);
|
|
147
|
+
await uploadRender(getClient(), md5, readable, folderSize);
|
|
146
148
|
process.stderr.write("Render assets uploaded\n");
|
|
147
149
|
process.stderr.write(inspect(render));
|
|
148
150
|
process.stderr.write("\n");
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { program, Option } from "commander";
|
|
2
|
+
import ora from "ora";
|
|
3
|
+
import chalk from "chalk";
|
|
4
|
+
import debug from "debug";
|
|
5
|
+
import { input, select } from "@inquirer/prompts";
|
|
6
|
+
import { getClient } from "../utils/index.js";
|
|
7
|
+
const log = debug("ef:cli:auth");
|
|
8
|
+
const topics = [
|
|
9
|
+
"render.created",
|
|
10
|
+
"render.rendering",
|
|
11
|
+
"render.pending",
|
|
12
|
+
"render.failed",
|
|
13
|
+
"render.completed"
|
|
14
|
+
];
|
|
15
|
+
const testWebhookURL = async ({
|
|
16
|
+
webhookURL,
|
|
17
|
+
topic
|
|
18
|
+
}) => {
|
|
19
|
+
const response = await getClient().authenticatedFetch(
|
|
20
|
+
"/api/v1/test_webhook",
|
|
21
|
+
{
|
|
22
|
+
method: "POST",
|
|
23
|
+
body: JSON.stringify({
|
|
24
|
+
webhookURL,
|
|
25
|
+
topic
|
|
26
|
+
})
|
|
27
|
+
}
|
|
28
|
+
);
|
|
29
|
+
return response.json();
|
|
30
|
+
};
|
|
31
|
+
const webhookCommand = program.command("webhook").description("Test webhook URL with a topic").option("-u, --webhookURL <webhookURL>", "Webhook URL").addOption(new Option("-t, --topic <topic>", "Topic").choices(topics)).action(async () => {
|
|
32
|
+
const options = webhookCommand.opts();
|
|
33
|
+
log("Options:", options);
|
|
34
|
+
let { webhookURL, topic } = options;
|
|
35
|
+
if (!webhookURL) {
|
|
36
|
+
const answer = await input({ message: "Enter a webhook URL:" });
|
|
37
|
+
webhookURL = answer;
|
|
38
|
+
}
|
|
39
|
+
if (!topic) {
|
|
40
|
+
const answer = await select({
|
|
41
|
+
message: "Select a topic:",
|
|
42
|
+
choices: [...topics.map((topic2) => ({ title: topic2, value: topic2 }))]
|
|
43
|
+
});
|
|
44
|
+
topic = answer;
|
|
45
|
+
}
|
|
46
|
+
const spinner = ora("Testing...").start();
|
|
47
|
+
try {
|
|
48
|
+
const apiData = await testWebhookURL({ webhookURL, topic });
|
|
49
|
+
spinner.succeed("Webhook URL is working! š");
|
|
50
|
+
process.stderr.write(chalk.green(`${apiData.message}
|
|
51
|
+
`));
|
|
52
|
+
} catch (error) {
|
|
53
|
+
spinner.fail("Webhook URL is not working!");
|
|
54
|
+
process.stderr.write(error?.message);
|
|
55
|
+
process.stderr.write("\n");
|
|
56
|
+
log("Error:", error);
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
export {
|
|
60
|
+
testWebhookURL
|
|
61
|
+
};
|
package/dist/index.d.ts
CHANGED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
|
package/dist/index.js
CHANGED
|
@@ -9,6 +9,7 @@ import "./commands/preview.js";
|
|
|
9
9
|
import "./commands/process.js";
|
|
10
10
|
import "./commands/process-file.js";
|
|
11
11
|
import "./commands/check.js";
|
|
12
|
+
import "./commands/webhook.js";
|
|
12
13
|
program.name("editframe").addOption(new Option("-t, --token <token>", "API Token").env("EF_TOKEN")).addOption(
|
|
13
14
|
new Option("--ef-host <host>", "Editframe Host").env("EF_HOST").default("https://editframe.dev")
|
|
14
15
|
).addOption(
|
|
@@ -2,7 +2,7 @@ import { Probe } from "@editframe/assets";
|
|
|
2
2
|
import fs from "node:fs/promises";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { createCaptionFile, uploadCaptionFile, createISOBMFFFile, uploadFragmentIndex, createISOBMFFTrack, uploadISOBMFFTrack, createImageFile, uploadImageFile } from "@editframe/api";
|
|
5
|
-
import { createReadStream } from "node:fs";
|
|
5
|
+
import { createReadStream, statSync } from "node:fs";
|
|
6
6
|
import { getClient } from "../utils/index.js";
|
|
7
7
|
const imageMatch = /\.(png|jpe?g|gif|webp)$/i;
|
|
8
8
|
const trackMatch = /\.track-[\d]+.mp4$/i;
|
|
@@ -49,13 +49,27 @@ const syncAssetDirectory = async (projectDirectory) => {
|
|
|
49
49
|
const assets = await fs.readdir(fullPath);
|
|
50
50
|
process.stderr.write(`Syncing asset dir: ${fullPath}
|
|
51
51
|
`);
|
|
52
|
-
|
|
53
|
-
|
|
52
|
+
const errors = {};
|
|
53
|
+
const reportError = (path2, message) => {
|
|
54
|
+
errors[path2] ||= [];
|
|
55
|
+
errors[path2].push(message);
|
|
56
|
+
process.stderr.write(` š« ${message}
|
|
57
|
+
`);
|
|
58
|
+
};
|
|
59
|
+
const reportSuccess = (_path, message) => {
|
|
60
|
+
process.stderr.write(` ā
${message}
|
|
54
61
|
`);
|
|
62
|
+
};
|
|
63
|
+
const reportInfo = (_path, message) => {
|
|
64
|
+
process.stderr.write(` ${message}
|
|
65
|
+
`);
|
|
66
|
+
};
|
|
67
|
+
for (const asset of assets) {
|
|
68
|
+
reportInfo(asset, `Syncing asset: ${asset}`);
|
|
55
69
|
const assetDir = path.join(fullPath, asset);
|
|
56
70
|
const stat2 = await fs.stat(assetDir);
|
|
57
71
|
if (!stat2.isDirectory()) {
|
|
58
|
-
|
|
72
|
+
reportError(asset, "Invalid asset. Did not find asset directory.");
|
|
59
73
|
return;
|
|
60
74
|
}
|
|
61
75
|
const subAssets = await fs.readdir(assetDir);
|
|
@@ -66,9 +80,9 @@ const syncAssetDirectory = async (projectDirectory) => {
|
|
|
66
80
|
const subAssetPath = path.join(assetDir, subAsset);
|
|
67
81
|
const syncStatus = new SyncStatus(subAssetPath);
|
|
68
82
|
if (await syncStatus.isSynced()) {
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
`
|
|
83
|
+
reportInfo(
|
|
84
|
+
subAsset,
|
|
85
|
+
`ā Sub-asset has already been synced: ${subAsset}`
|
|
72
86
|
);
|
|
73
87
|
continue;
|
|
74
88
|
}
|
|
@@ -76,46 +90,50 @@ const syncAssetDirectory = async (projectDirectory) => {
|
|
|
76
90
|
case imageMatch.test(subAsset): {
|
|
77
91
|
const probeResult = await Probe.probePath(subAssetPath);
|
|
78
92
|
const [videoProbe] = probeResult.videoStreams;
|
|
93
|
+
const { format } = probeResult;
|
|
79
94
|
if (!videoProbe) {
|
|
80
|
-
|
|
81
|
-
`š« No video stream found in image: ${subAsset}
|
|
82
|
-
`
|
|
83
|
-
);
|
|
95
|
+
reportError(subAsset, `No media info found in image: ${subAsset}`);
|
|
84
96
|
break;
|
|
85
97
|
}
|
|
86
98
|
const ext = path.extname(subAsset).slice(1);
|
|
87
99
|
if (!(ext === "jpg" || ext === "jpeg" || ext === "png" || ext === "webp")) {
|
|
88
|
-
|
|
89
|
-
`);
|
|
100
|
+
reportError(subAsset, `Invalid image format: ${subAsset}`);
|
|
90
101
|
break;
|
|
91
102
|
}
|
|
92
|
-
|
|
93
|
-
`);
|
|
103
|
+
reportInfo(subAsset, `š¼ļø Syncing image: ${subAsset}`);
|
|
94
104
|
const created = await createImageFile(getClient(), {
|
|
95
105
|
id: asset,
|
|
96
106
|
filename: subAsset,
|
|
97
107
|
width: videoProbe.width,
|
|
98
108
|
height: videoProbe.height,
|
|
99
|
-
mime_type: `image/${ext}
|
|
109
|
+
mime_type: `image/${ext}`,
|
|
110
|
+
byte_size: (await fs.stat(subAssetPath)).size
|
|
100
111
|
});
|
|
101
112
|
if (created) {
|
|
102
113
|
if (created.complete) {
|
|
103
|
-
|
|
114
|
+
reportInfo(subAsset, " ā Image has already been synced");
|
|
115
|
+
await syncStatus.markSynced();
|
|
104
116
|
} else {
|
|
105
117
|
await uploadImageFile(
|
|
106
118
|
getClient(),
|
|
107
119
|
created.id,
|
|
108
|
-
createReadStream(subAssetPath)
|
|
109
|
-
|
|
110
|
-
|
|
120
|
+
createReadStream(subAssetPath),
|
|
121
|
+
Number.parseInt(format.size || "0")
|
|
122
|
+
).then(() => {
|
|
123
|
+
reportSuccess(subAsset, "Image has been synced.");
|
|
124
|
+
return syncStatus.markSynced();
|
|
125
|
+
}).catch((error) => {
|
|
126
|
+
reportError(
|
|
127
|
+
subAsset,
|
|
128
|
+
`Error uploading image: ${error.message}`
|
|
129
|
+
);
|
|
130
|
+
});
|
|
111
131
|
}
|
|
112
|
-
await syncStatus.markSynced();
|
|
113
132
|
}
|
|
114
133
|
break;
|
|
115
134
|
}
|
|
116
135
|
case trackMatch.test(subAsset): {
|
|
117
|
-
|
|
118
|
-
`);
|
|
136
|
+
reportInfo(subAsset, `š¼ Syncing a/v track: ${subAsset}`);
|
|
119
137
|
const createdFile = await createISOBMFFFile(getClient(), {
|
|
120
138
|
id: asset,
|
|
121
139
|
filename: subAsset.replace(/\.track-[\d]+.mp4$/, "")
|
|
@@ -124,25 +142,19 @@ const syncAssetDirectory = async (projectDirectory) => {
|
|
|
124
142
|
const probe = await Probe.probePath(subAssetPath);
|
|
125
143
|
const trackId = subAsset.match(/track-([\d]+).mp4/)?.[1];
|
|
126
144
|
if (!trackId) {
|
|
127
|
-
|
|
128
|
-
`š« No track ID found for track: ${subAsset}
|
|
129
|
-
`
|
|
130
|
-
);
|
|
145
|
+
reportError(subAsset, `No track ID found for track: ${subAsset}`);
|
|
131
146
|
break;
|
|
132
147
|
}
|
|
133
148
|
const [track] = probe.streams;
|
|
134
149
|
if (!track) {
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
`
|
|
150
|
+
reportError(
|
|
151
|
+
subAsset,
|
|
152
|
+
`No track stream found in track: ${subAsset}`
|
|
138
153
|
);
|
|
139
154
|
break;
|
|
140
155
|
}
|
|
141
156
|
if (track.duration === void 0) {
|
|
142
|
-
|
|
143
|
-
`š« No duration found in track: ${subAsset}
|
|
144
|
-
`
|
|
145
|
-
);
|
|
157
|
+
reportError(subAsset, `No duration found in track: ${subAsset}`);
|
|
146
158
|
break;
|
|
147
159
|
}
|
|
148
160
|
const stat3 = await fs.stat(subAssetPath);
|
|
@@ -168,82 +180,122 @@ const syncAssetDirectory = async (projectDirectory) => {
|
|
|
168
180
|
createPayload
|
|
169
181
|
);
|
|
170
182
|
if (createdTrack) {
|
|
171
|
-
if (createdTrack.
|
|
172
|
-
|
|
183
|
+
if (createdTrack.next_byte === createdTrack.byte_size) {
|
|
184
|
+
reportInfo(subAsset, "ā Track has already been synced.");
|
|
185
|
+
await syncStatus.markSynced();
|
|
173
186
|
} else {
|
|
174
187
|
await uploadISOBMFFTrack(
|
|
175
188
|
getClient(),
|
|
176
189
|
createdFile.id,
|
|
177
190
|
Number(trackId),
|
|
178
|
-
createReadStream(subAssetPath)
|
|
179
|
-
|
|
180
|
-
|
|
191
|
+
createReadStream(subAssetPath),
|
|
192
|
+
createdTrack.byte_size
|
|
193
|
+
).then(() => {
|
|
194
|
+
reportSuccess(subAsset, "Track has been synced.");
|
|
195
|
+
return syncStatus.markSynced();
|
|
196
|
+
}).catch((error) => {
|
|
197
|
+
reportError(
|
|
198
|
+
subAsset,
|
|
199
|
+
`Error uploading track: ${error.message}`
|
|
200
|
+
);
|
|
201
|
+
});
|
|
181
202
|
}
|
|
182
|
-
await syncStatus.markSynced();
|
|
183
203
|
}
|
|
184
204
|
}
|
|
185
205
|
break;
|
|
186
206
|
}
|
|
187
207
|
case fragmentIndexMatch.test(subAsset): {
|
|
188
|
-
|
|
189
|
-
`);
|
|
208
|
+
reportInfo(subAsset, `š Syncing fragment index: ${subAsset}`);
|
|
190
209
|
const createdFile = await createISOBMFFFile(getClient(), {
|
|
191
210
|
id: asset,
|
|
192
211
|
filename: subAsset.replace(/\.tracks.json$/, "")
|
|
193
212
|
});
|
|
194
213
|
if (createdFile) {
|
|
195
214
|
if (createdFile.fragment_index_complete) {
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
);
|
|
215
|
+
reportInfo(subAsset, "ā Fragment index has already been synced.");
|
|
216
|
+
await syncStatus.markSynced();
|
|
199
217
|
} else {
|
|
218
|
+
const stats = statSync(subAssetPath);
|
|
200
219
|
const readStream = createReadStream(subAssetPath);
|
|
201
|
-
await uploadFragmentIndex(
|
|
202
|
-
|
|
220
|
+
await uploadFragmentIndex(
|
|
221
|
+
getClient(),
|
|
222
|
+
asset,
|
|
223
|
+
readStream,
|
|
224
|
+
stats.size
|
|
225
|
+
).then(() => {
|
|
226
|
+
reportSuccess(subAsset, "Fragment index has been synced.");
|
|
227
|
+
return syncStatus.markSynced();
|
|
228
|
+
}).catch((error) => {
|
|
229
|
+
reportError(
|
|
230
|
+
subAsset,
|
|
231
|
+
`Error uploading fragment index: ${error.message}`
|
|
232
|
+
);
|
|
233
|
+
});
|
|
203
234
|
}
|
|
204
|
-
await syncStatus.markSynced();
|
|
205
235
|
} else {
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
`
|
|
236
|
+
reportError(
|
|
237
|
+
subAsset,
|
|
238
|
+
`No file found for fragment index: ${subAsset}`
|
|
209
239
|
);
|
|
210
240
|
break;
|
|
211
241
|
}
|
|
212
242
|
break;
|
|
213
243
|
}
|
|
214
244
|
case captionsMatch.test(subAsset): {
|
|
215
|
-
|
|
216
|
-
`);
|
|
245
|
+
reportInfo(subAsset, `š Syncing captions: ${subAsset}`);
|
|
217
246
|
const createdFile = await createCaptionFile(getClient(), {
|
|
218
247
|
id: asset,
|
|
219
|
-
filename: subAsset.replace(/\.captions.json$/, "")
|
|
248
|
+
filename: subAsset.replace(/\.captions.json$/, ""),
|
|
249
|
+
byte_size: (await fs.stat(subAsset)).size
|
|
220
250
|
});
|
|
221
251
|
if (createdFile) {
|
|
222
252
|
if (createdFile.complete) {
|
|
223
|
-
|
|
253
|
+
reportInfo(subAsset, "ā Captions have already been synced.");
|
|
254
|
+
await syncStatus.markSynced();
|
|
224
255
|
} else {
|
|
225
256
|
const readStream = createReadStream(subAssetPath);
|
|
226
|
-
|
|
227
|
-
|
|
257
|
+
const stats = statSync(subAssetPath);
|
|
258
|
+
await uploadCaptionFile(
|
|
259
|
+
getClient(),
|
|
260
|
+
asset,
|
|
261
|
+
readStream,
|
|
262
|
+
stats.size
|
|
263
|
+
).then(() => {
|
|
264
|
+
reportSuccess(subAsset, "Captions have been synced.");
|
|
265
|
+
return syncStatus.markSynced();
|
|
266
|
+
}).catch((error) => {
|
|
267
|
+
reportError(
|
|
268
|
+
subAsset,
|
|
269
|
+
`Error uploading captions: ${error.message}`
|
|
270
|
+
);
|
|
271
|
+
});
|
|
228
272
|
}
|
|
229
|
-
await syncStatus.markSynced();
|
|
230
273
|
} else {
|
|
231
|
-
|
|
232
|
-
`š« No file found for captions: ${subAsset}
|
|
233
|
-
`
|
|
234
|
-
);
|
|
274
|
+
reportError(subAsset, `No file found for captions: ${subAsset}`);
|
|
235
275
|
break;
|
|
236
276
|
}
|
|
237
277
|
break;
|
|
238
278
|
}
|
|
239
279
|
default: {
|
|
240
|
-
|
|
241
|
-
`);
|
|
280
|
+
reportError(subAsset, `Unknown sub-asset: ${subAsset}`);
|
|
242
281
|
break;
|
|
243
282
|
}
|
|
244
283
|
}
|
|
245
284
|
}
|
|
246
285
|
}
|
|
286
|
+
if (Object.keys(errors).length) {
|
|
287
|
+
process.stderr.write("\n\nā Encountered errors while syncing assets:\n");
|
|
288
|
+
for (const [asset, messages] of Object.entries(errors)) {
|
|
289
|
+
process.stderr.write(`
|
|
290
|
+
š« ${asset}
|
|
291
|
+
`);
|
|
292
|
+
for (const message of messages) {
|
|
293
|
+
process.stderr.write(` - ${message}
|
|
294
|
+
`);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
throw new Error("Failed to sync assets");
|
|
298
|
+
}
|
|
247
299
|
};
|
|
248
300
|
export {
|
|
249
301
|
syncAssetDirectory
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const getFolderSize: (folderPath: string) => Promise<number>;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { promises } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
const getFolderSize = async (folderPath) => {
|
|
4
|
+
let totalSize = 0;
|
|
5
|
+
async function calculateSize(dir) {
|
|
6
|
+
const files = await promises.readdir(dir);
|
|
7
|
+
for (const file of files) {
|
|
8
|
+
const filePath = path.join(dir, file);
|
|
9
|
+
const stats = await promises.stat(filePath);
|
|
10
|
+
if (stats.isDirectory()) {
|
|
11
|
+
await calculateSize(filePath);
|
|
12
|
+
} else {
|
|
13
|
+
totalSize += stats.size;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
await calculateSize(folderPath);
|
|
18
|
+
return totalSize;
|
|
19
|
+
};
|
|
20
|
+
export {
|
|
21
|
+
getFolderSize
|
|
22
|
+
};
|
package/dist/utils/index.d.ts
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@editframe/cli",
|
|
3
|
-
"version": "0.8.0-beta.
|
|
3
|
+
"version": "0.8.0-beta.10",
|
|
4
4
|
"description": "Command line interface for EditFrame",
|
|
5
5
|
"bin": {
|
|
6
6
|
"editframe": "./dist/index.js"
|
|
@@ -19,14 +19,15 @@
|
|
|
19
19
|
"@types/promptly": "^3.0.5",
|
|
20
20
|
"@types/tar": "^6.1.13",
|
|
21
21
|
"typescript": "^5.5.4",
|
|
22
|
-
"vite-plugin-dts": "^
|
|
22
|
+
"vite-plugin-dts": "^4.0.3",
|
|
23
23
|
"vite-tsconfig-paths": "^4.3.2"
|
|
24
24
|
},
|
|
25
25
|
"dependencies": {
|
|
26
|
-
"@editframe/api": "0.8.0-beta.
|
|
27
|
-
"@editframe/assets": "0.8.0-beta.
|
|
28
|
-
"@editframe/elements": "0.8.0-beta.
|
|
29
|
-
"@editframe/vite-plugin": "0.8.0-beta.
|
|
26
|
+
"@editframe/api": "0.8.0-beta.10",
|
|
27
|
+
"@editframe/assets": "0.8.0-beta.10",
|
|
28
|
+
"@editframe/elements": "0.8.0-beta.10",
|
|
29
|
+
"@editframe/vite-plugin": "0.8.0-beta.10",
|
|
30
|
+
"@inquirer/prompts": "^5.3.8",
|
|
30
31
|
"axios": "^1.6.8",
|
|
31
32
|
"chalk": "^5.3.0",
|
|
32
33
|
"commander": "^12.0.0",
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { basename } from "node:path";
|
|
2
|
+
import { stat } from "node:fs/promises";
|
|
3
|
+
import { createReadStream } from "node:fs";
|
|
2
4
|
|
|
3
5
|
import { program } from "commander";
|
|
4
6
|
|
|
5
7
|
import { withSpinner } from "../utils/withSpinner.ts";
|
|
6
8
|
|
|
7
9
|
import { getClient } from "../utils/index.ts";
|
|
8
|
-
import { createReadStream } from "node:fs";
|
|
9
10
|
import {
|
|
10
11
|
createUnprocessedFile,
|
|
11
12
|
updateUnprocessedFile,
|
|
@@ -21,18 +22,21 @@ program
|
|
|
21
22
|
|
|
22
23
|
const fileId = await md5FilePath(path);
|
|
23
24
|
|
|
25
|
+
const byte_size = (await stat(path)).size;
|
|
26
|
+
|
|
24
27
|
await withSpinner("Creating unprocessed file record", async () => {
|
|
25
28
|
await createUnprocessedFile(client, {
|
|
26
29
|
id: fileId,
|
|
27
30
|
processes: [],
|
|
28
31
|
filename: basename(path),
|
|
32
|
+
byte_size,
|
|
29
33
|
});
|
|
30
34
|
});
|
|
31
35
|
|
|
32
36
|
const readStream = createReadStream(path);
|
|
33
37
|
|
|
34
38
|
await withSpinner("Uploading file", async () => {
|
|
35
|
-
await uploadUnprocessedFile(client, fileId, readStream);
|
|
39
|
+
await uploadUnprocessedFile(client, fileId, readStream, byte_size);
|
|
36
40
|
});
|
|
37
41
|
|
|
38
42
|
const unprocessedFile = await withSpinner(
|
package/src/commands/render.ts
CHANGED
|
@@ -19,6 +19,7 @@ import { getClient } from "../utils/index.ts";
|
|
|
19
19
|
import { getRenderInfo } from "../operations/getRenderInfo.ts";
|
|
20
20
|
import { processRenderInfo } from "../operations/processRenderInfo.ts";
|
|
21
21
|
import { validateVideoResolution } from "../utils/validateVideoResolution.ts";
|
|
22
|
+
import { getFolderSize } from "../utils/getFolderSize.ts";
|
|
22
23
|
|
|
23
24
|
interface StrategyBuilder {
|
|
24
25
|
buildProductionUrl: (tagName: string, assetPath: string) => Promise<string>;
|
|
@@ -179,7 +180,8 @@ program
|
|
|
179
180
|
);
|
|
180
181
|
const readable = new PassThrough();
|
|
181
182
|
tarStream.pipe(readable);
|
|
182
|
-
await
|
|
183
|
+
const folderSize = await getFolderSize(distDir);
|
|
184
|
+
await uploadRender(getClient(), md5, readable, folderSize);
|
|
183
185
|
process.stderr.write("Render assets uploaded\n");
|
|
184
186
|
process.stderr.write(inspect(render));
|
|
185
187
|
process.stderr.write("\n");
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { program, Option } from "commander";
|
|
2
|
+
import ora from "ora";
|
|
3
|
+
import chalk from "chalk";
|
|
4
|
+
import debug from "debug";
|
|
5
|
+
import { input, select } from "@inquirer/prompts";
|
|
6
|
+
|
|
7
|
+
import { getClient } from "../utils/index.ts";
|
|
8
|
+
|
|
9
|
+
const log = debug("ef:cli:auth");
|
|
10
|
+
|
|
11
|
+
export interface APITestWebhhokResult {
|
|
12
|
+
message: string;
|
|
13
|
+
}
|
|
14
|
+
const topics = [
|
|
15
|
+
"render.created",
|
|
16
|
+
"render.rendering",
|
|
17
|
+
"render.pending",
|
|
18
|
+
"render.failed",
|
|
19
|
+
"render.completed",
|
|
20
|
+
];
|
|
21
|
+
|
|
22
|
+
export const testWebhookURL = async ({
|
|
23
|
+
webhookURL,
|
|
24
|
+
topic,
|
|
25
|
+
}: {
|
|
26
|
+
webhookURL: string;
|
|
27
|
+
topic: string;
|
|
28
|
+
}) => {
|
|
29
|
+
const response = await getClient().authenticatedFetch(
|
|
30
|
+
"/api/v1/test_webhook",
|
|
31
|
+
{
|
|
32
|
+
method: "POST",
|
|
33
|
+
body: JSON.stringify({
|
|
34
|
+
webhookURL,
|
|
35
|
+
topic,
|
|
36
|
+
}),
|
|
37
|
+
},
|
|
38
|
+
);
|
|
39
|
+
return response.json() as Promise<APITestWebhhokResult>;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const webhookCommand = program
|
|
43
|
+
.command("webhook")
|
|
44
|
+
.description("Test webhook URL with a topic")
|
|
45
|
+
.option("-u, --webhookURL <webhookURL>", "Webhook URL")
|
|
46
|
+
.addOption(new Option("-t, --topic <topic>", "Topic").choices(topics))
|
|
47
|
+
.action(async () => {
|
|
48
|
+
const options = webhookCommand.opts();
|
|
49
|
+
log("Options:", options);
|
|
50
|
+
let { webhookURL, topic } = options;
|
|
51
|
+
|
|
52
|
+
if (!webhookURL) {
|
|
53
|
+
const answer = await input({ message: "Enter a webhook URL:" });
|
|
54
|
+
webhookURL = answer;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (!topic) {
|
|
58
|
+
const answer = await select({
|
|
59
|
+
message: "Select a topic:",
|
|
60
|
+
choices: [...topics.map((topic) => ({ title: topic, value: topic }))],
|
|
61
|
+
});
|
|
62
|
+
topic = answer;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const spinner = ora("Testing...").start();
|
|
66
|
+
try {
|
|
67
|
+
const apiData = await testWebhookURL({ webhookURL, topic });
|
|
68
|
+
spinner.succeed("Webhook URL is working! š");
|
|
69
|
+
process.stderr.write(chalk.green(`${apiData.message}\n`));
|
|
70
|
+
} catch (error: any) {
|
|
71
|
+
spinner.fail("Webhook URL is not working!");
|
|
72
|
+
process.stderr.write(error?.message);
|
|
73
|
+
process.stderr.write("\n");
|
|
74
|
+
log("Error:", error);
|
|
75
|
+
}
|
|
76
|
+
});
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
uploadISOBMFFTrack,
|
|
13
13
|
type CreateISOBMFFTrackPayload,
|
|
14
14
|
} from "@editframe/api";
|
|
15
|
+
import { statSync } from "node:fs";
|
|
15
16
|
|
|
16
17
|
import { createReadStream } from "node:fs";
|
|
17
18
|
import type { z } from "zod";
|
|
@@ -62,6 +63,7 @@ export const syncAssetDirectory = async (
|
|
|
62
63
|
}
|
|
63
64
|
throw error;
|
|
64
65
|
});
|
|
66
|
+
|
|
65
67
|
if (!stat?.isDirectory()) {
|
|
66
68
|
console.error(`No assets cache directory found at ${fullPath}`);
|
|
67
69
|
return;
|
|
@@ -70,12 +72,28 @@ export const syncAssetDirectory = async (
|
|
|
70
72
|
|
|
71
73
|
process.stderr.write(`Syncing asset dir: ${fullPath}\n`);
|
|
72
74
|
|
|
75
|
+
const errors: Record<string, string[]> = {};
|
|
76
|
+
|
|
77
|
+
const reportError = (path: string, message: string) => {
|
|
78
|
+
errors[path] ||= [];
|
|
79
|
+
errors[path].push(message);
|
|
80
|
+
process.stderr.write(` š« ${message}\n`);
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
const reportSuccess = (_path: string, message: string) => {
|
|
84
|
+
process.stderr.write(` ā
${message}\n`);
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const reportInfo = (_path: string, message: string) => {
|
|
88
|
+
process.stderr.write(` ${message}\n`);
|
|
89
|
+
};
|
|
90
|
+
|
|
73
91
|
for (const asset of assets) {
|
|
74
|
-
|
|
92
|
+
reportInfo(asset, `Syncing asset: ${asset}`);
|
|
75
93
|
const assetDir = path.join(fullPath, asset);
|
|
76
94
|
const stat = await fs.stat(assetDir);
|
|
77
95
|
if (!stat.isDirectory()) {
|
|
78
|
-
|
|
96
|
+
reportError(asset, "Invalid asset. Did not find asset directory.");
|
|
79
97
|
return;
|
|
80
98
|
}
|
|
81
99
|
const subAssets = await fs.readdir(assetDir);
|
|
@@ -88,8 +106,9 @@ export const syncAssetDirectory = async (
|
|
|
88
106
|
const subAssetPath = path.join(assetDir, subAsset);
|
|
89
107
|
const syncStatus = new SyncStatus(subAssetPath);
|
|
90
108
|
if (await syncStatus.isSynced()) {
|
|
91
|
-
|
|
92
|
-
|
|
109
|
+
reportInfo(
|
|
110
|
+
subAsset,
|
|
111
|
+
`ā Sub-asset has already been synced: ${subAsset}`,
|
|
93
112
|
);
|
|
94
113
|
continue;
|
|
95
114
|
}
|
|
@@ -97,10 +116,9 @@ export const syncAssetDirectory = async (
|
|
|
97
116
|
case imageMatch.test(subAsset): {
|
|
98
117
|
const probeResult = await Probe.probePath(subAssetPath);
|
|
99
118
|
const [videoProbe] = probeResult.videoStreams;
|
|
119
|
+
const { format } = probeResult;
|
|
100
120
|
if (!videoProbe) {
|
|
101
|
-
|
|
102
|
-
`š« No video stream found in image: ${subAsset}\n`,
|
|
103
|
-
);
|
|
121
|
+
reportError(subAsset, `No media info found in image: ${subAsset}`);
|
|
104
122
|
break;
|
|
105
123
|
}
|
|
106
124
|
const ext = path.extname(subAsset).slice(1);
|
|
@@ -112,34 +130,45 @@ export const syncAssetDirectory = async (
|
|
|
112
130
|
ext === "webp"
|
|
113
131
|
)
|
|
114
132
|
) {
|
|
115
|
-
|
|
133
|
+
reportError(subAsset, `Invalid image format: ${subAsset}`);
|
|
116
134
|
break;
|
|
117
135
|
}
|
|
118
|
-
|
|
136
|
+
reportInfo(subAsset, `š¼ļø Syncing image: ${subAsset}`);
|
|
119
137
|
const created = await createImageFile(getClient(), {
|
|
120
138
|
id: asset,
|
|
121
139
|
filename: subAsset,
|
|
122
140
|
width: videoProbe.width,
|
|
123
141
|
height: videoProbe.height,
|
|
124
142
|
mime_type: `image/${ext}`,
|
|
143
|
+
byte_size: (await fs.stat(subAssetPath)).size,
|
|
125
144
|
});
|
|
126
145
|
if (created) {
|
|
127
146
|
if (created.complete) {
|
|
128
|
-
|
|
147
|
+
reportInfo(subAsset, " ā Image has already been synced");
|
|
148
|
+
await syncStatus.markSynced();
|
|
129
149
|
} else {
|
|
130
150
|
await uploadImageFile(
|
|
131
151
|
getClient(),
|
|
132
152
|
created.id,
|
|
133
153
|
createReadStream(subAssetPath),
|
|
134
|
-
|
|
135
|
-
|
|
154
|
+
Number.parseInt(format.size || "0"),
|
|
155
|
+
)
|
|
156
|
+
.then(() => {
|
|
157
|
+
reportSuccess(subAsset, "Image has been synced.");
|
|
158
|
+
return syncStatus.markSynced();
|
|
159
|
+
})
|
|
160
|
+
.catch((error) => {
|
|
161
|
+
reportError(
|
|
162
|
+
subAsset,
|
|
163
|
+
`Error uploading image: ${error.message}`,
|
|
164
|
+
);
|
|
165
|
+
});
|
|
136
166
|
}
|
|
137
|
-
await syncStatus.markSynced();
|
|
138
167
|
}
|
|
139
168
|
break;
|
|
140
169
|
}
|
|
141
170
|
case trackMatch.test(subAsset): {
|
|
142
|
-
|
|
171
|
+
reportInfo(subAsset, `š¼ Syncing a/v track: ${subAsset}`);
|
|
143
172
|
const createdFile = await createISOBMFFFile(getClient(), {
|
|
144
173
|
id: asset,
|
|
145
174
|
filename: subAsset.replace(/\.track-[\d]+.mp4$/, ""),
|
|
@@ -148,29 +177,26 @@ export const syncAssetDirectory = async (
|
|
|
148
177
|
const probe = await Probe.probePath(subAssetPath);
|
|
149
178
|
const trackId = subAsset.match(/track-([\d]+).mp4/)?.[1];
|
|
150
179
|
if (!trackId) {
|
|
151
|
-
|
|
152
|
-
`š« No track ID found for track: ${subAsset}\n`,
|
|
153
|
-
);
|
|
180
|
+
reportError(subAsset, `No track ID found for track: ${subAsset}`);
|
|
154
181
|
break;
|
|
155
182
|
}
|
|
156
183
|
const [track] = probe.streams;
|
|
157
184
|
if (!track) {
|
|
158
|
-
|
|
159
|
-
|
|
185
|
+
reportError(
|
|
186
|
+
subAsset,
|
|
187
|
+
`No track stream found in track: ${subAsset}`,
|
|
160
188
|
);
|
|
161
189
|
break;
|
|
162
190
|
}
|
|
163
191
|
if (track.duration === undefined) {
|
|
164
|
-
|
|
165
|
-
`š« No duration found in track: ${subAsset}\n`,
|
|
166
|
-
);
|
|
192
|
+
reportError(subAsset, `No duration found in track: ${subAsset}`);
|
|
167
193
|
break;
|
|
168
194
|
}
|
|
169
195
|
|
|
170
196
|
const stat = await fs.stat(subAssetPath);
|
|
171
197
|
|
|
172
198
|
/**
|
|
173
|
-
* Because the payload is a discriminated union, we
|
|
199
|
+
* Because the payload is a discriminated union, we need to create these objects
|
|
174
200
|
* only after the value for type has been narrowed, otherwise the object will
|
|
175
201
|
* have type: "audio" | "video" which doesnt' match the payload type that
|
|
176
202
|
* looks more like { type: "audio", ... } | { type: "video", ... }
|
|
@@ -201,79 +227,126 @@ export const syncAssetDirectory = async (
|
|
|
201
227
|
);
|
|
202
228
|
|
|
203
229
|
if (createdTrack) {
|
|
204
|
-
if (
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
) {
|
|
208
|
-
process.stderr.write(" ā Track has already been synced.\n");
|
|
230
|
+
if (createdTrack.next_byte === createdTrack.byte_size) {
|
|
231
|
+
reportInfo(subAsset, "ā Track has already been synced.");
|
|
232
|
+
await syncStatus.markSynced();
|
|
209
233
|
} else {
|
|
210
234
|
await uploadISOBMFFTrack(
|
|
211
235
|
getClient(),
|
|
212
236
|
createdFile.id,
|
|
213
237
|
Number(trackId),
|
|
214
238
|
createReadStream(subAssetPath),
|
|
215
|
-
|
|
216
|
-
|
|
239
|
+
createdTrack.byte_size,
|
|
240
|
+
)
|
|
241
|
+
.then(() => {
|
|
242
|
+
reportSuccess(subAsset, "Track has been synced.");
|
|
243
|
+
return syncStatus.markSynced();
|
|
244
|
+
})
|
|
245
|
+
.catch((error) => {
|
|
246
|
+
reportError(
|
|
247
|
+
subAsset,
|
|
248
|
+
`Error uploading track: ${error.message}`,
|
|
249
|
+
);
|
|
250
|
+
});
|
|
217
251
|
}
|
|
218
|
-
await syncStatus.markSynced();
|
|
219
252
|
}
|
|
220
253
|
}
|
|
221
254
|
|
|
222
255
|
break;
|
|
223
256
|
}
|
|
224
257
|
case fragmentIndexMatch.test(subAsset): {
|
|
225
|
-
|
|
258
|
+
reportInfo(subAsset, `š Syncing fragment index: ${subAsset}`);
|
|
226
259
|
const createdFile = await createISOBMFFFile(getClient(), {
|
|
227
260
|
id: asset,
|
|
228
261
|
filename: subAsset.replace(/\.tracks.json$/, ""),
|
|
229
262
|
});
|
|
230
263
|
if (createdFile) {
|
|
231
264
|
if (createdFile.fragment_index_complete) {
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
);
|
|
265
|
+
reportInfo(subAsset, "ā Fragment index has already been synced.");
|
|
266
|
+
await syncStatus.markSynced();
|
|
235
267
|
} else {
|
|
268
|
+
const stats = statSync(subAssetPath);
|
|
236
269
|
const readStream = createReadStream(subAssetPath);
|
|
237
|
-
await uploadFragmentIndex(
|
|
238
|
-
|
|
270
|
+
await uploadFragmentIndex(
|
|
271
|
+
getClient(),
|
|
272
|
+
asset,
|
|
273
|
+
readStream,
|
|
274
|
+
stats.size,
|
|
275
|
+
)
|
|
276
|
+
.then(() => {
|
|
277
|
+
reportSuccess(subAsset, "Fragment index has been synced.");
|
|
278
|
+
return syncStatus.markSynced();
|
|
279
|
+
})
|
|
280
|
+
.catch((error) => {
|
|
281
|
+
reportError(
|
|
282
|
+
subAsset,
|
|
283
|
+
`Error uploading fragment index: ${error.message}`,
|
|
284
|
+
);
|
|
285
|
+
});
|
|
239
286
|
}
|
|
240
|
-
await syncStatus.markSynced();
|
|
241
287
|
} else {
|
|
242
|
-
|
|
243
|
-
|
|
288
|
+
reportError(
|
|
289
|
+
subAsset,
|
|
290
|
+
`No file found for fragment index: ${subAsset}`,
|
|
244
291
|
);
|
|
245
292
|
break;
|
|
246
293
|
}
|
|
247
294
|
break;
|
|
248
295
|
}
|
|
249
296
|
case captionsMatch.test(subAsset): {
|
|
250
|
-
|
|
297
|
+
reportInfo(subAsset, `š Syncing captions: ${subAsset}`);
|
|
251
298
|
const createdFile = await createCaptionFile(getClient(), {
|
|
252
299
|
id: asset,
|
|
253
300
|
filename: subAsset.replace(/\.captions.json$/, ""),
|
|
301
|
+
byte_size: (await fs.stat(subAsset)).size,
|
|
254
302
|
});
|
|
255
303
|
if (createdFile) {
|
|
256
304
|
if (createdFile.complete) {
|
|
257
|
-
|
|
305
|
+
reportInfo(subAsset, "ā Captions have already been synced.");
|
|
306
|
+
await syncStatus.markSynced();
|
|
258
307
|
} else {
|
|
259
308
|
const readStream = createReadStream(subAssetPath);
|
|
260
|
-
|
|
261
|
-
|
|
309
|
+
const stats = statSync(subAssetPath);
|
|
310
|
+
await uploadCaptionFile(
|
|
311
|
+
getClient(),
|
|
312
|
+
asset,
|
|
313
|
+
readStream,
|
|
314
|
+
stats.size,
|
|
315
|
+
)
|
|
316
|
+
.then(() => {
|
|
317
|
+
reportSuccess(subAsset, "Captions have been synced.");
|
|
318
|
+
return syncStatus.markSynced();
|
|
319
|
+
})
|
|
320
|
+
.catch((error) => {
|
|
321
|
+
reportError(
|
|
322
|
+
subAsset,
|
|
323
|
+
`Error uploading captions: ${error.message}`,
|
|
324
|
+
);
|
|
325
|
+
});
|
|
262
326
|
}
|
|
263
|
-
await syncStatus.markSynced();
|
|
264
327
|
} else {
|
|
265
|
-
|
|
266
|
-
`š« No file found for captions: ${subAsset}\n`,
|
|
267
|
-
);
|
|
328
|
+
reportError(subAsset, `No file found for captions: ${subAsset}`);
|
|
268
329
|
break;
|
|
269
330
|
}
|
|
270
331
|
break;
|
|
271
332
|
}
|
|
272
333
|
default: {
|
|
273
|
-
|
|
334
|
+
reportError(subAsset, `Unknown sub-asset: ${subAsset}`);
|
|
274
335
|
break;
|
|
275
336
|
}
|
|
276
337
|
}
|
|
277
338
|
}
|
|
278
339
|
}
|
|
340
|
+
|
|
341
|
+
if (Object.keys(errors).length) {
|
|
342
|
+
process.stderr.write("\n\nā Encountered errors while syncing assets:\n");
|
|
343
|
+
for (const [asset, messages] of Object.entries(errors)) {
|
|
344
|
+
process.stderr.write(`\nš« ${asset}\n`);
|
|
345
|
+
for (const message of messages) {
|
|
346
|
+
process.stderr.write(` - ${message}\n`);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
throw new Error("Failed to sync assets");
|
|
351
|
+
}
|
|
279
352
|
};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { promises as fs } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
export const getFolderSize = async (folderPath: string): Promise<number> => {
|
|
5
|
+
let totalSize = 0;
|
|
6
|
+
|
|
7
|
+
async function calculateSize(dir: string): Promise<void> {
|
|
8
|
+
const files = await fs.readdir(dir);
|
|
9
|
+
|
|
10
|
+
for (const file of files) {
|
|
11
|
+
const filePath = path.join(dir, file);
|
|
12
|
+
const stats = await fs.stat(filePath);
|
|
13
|
+
|
|
14
|
+
if (stats.isDirectory()) {
|
|
15
|
+
await calculateSize(filePath);
|
|
16
|
+
} else {
|
|
17
|
+
totalSize += stats.size;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
await calculateSize(folderPath);
|
|
23
|
+
return totalSize;
|
|
24
|
+
};
|