@arela/uploader 0.0.5 → 0.0.7
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/package.json +1 -1
- package/src/index.js +136 -21
package/package.json
CHANGED
package/src/index.js
CHANGED
|
@@ -5,9 +5,13 @@ import { config } from 'dotenv';
|
|
|
5
5
|
import fs from 'fs';
|
|
6
6
|
import { globby } from 'globby';
|
|
7
7
|
import mime from 'mime-types';
|
|
8
|
+
import { createRequire } from 'module';
|
|
8
9
|
import ora from 'ora';
|
|
9
10
|
import path from 'path';
|
|
10
11
|
|
|
12
|
+
const require = createRequire(import.meta.url);
|
|
13
|
+
const { version } = require('../package.json');
|
|
14
|
+
|
|
11
15
|
config();
|
|
12
16
|
|
|
13
17
|
const program = new Command();
|
|
@@ -16,10 +20,19 @@ const supabaseUrl = process.env.SUPABASE_URL;
|
|
|
16
20
|
const supabaseKey = process.env.SUPABASE_KEY;
|
|
17
21
|
const bucket = process.env.SUPABASE_BUCKET;
|
|
18
22
|
const basePath = process.env.UPLOAD_BASE_PATH;
|
|
19
|
-
const sources = process.env.UPLOAD_SOURCES?.split('|')
|
|
23
|
+
const sources = process.env.UPLOAD_SOURCES?.split('|')
|
|
24
|
+
.map((s) => s.trim())
|
|
25
|
+
.filter(Boolean);
|
|
20
26
|
|
|
21
27
|
const supabase = createClient(supabaseUrl, supabaseKey);
|
|
22
28
|
|
|
29
|
+
const sanitizePath = (path) =>
|
|
30
|
+
path
|
|
31
|
+
.replace(/[\\?%*:|"<>[\]~]/g, '-')
|
|
32
|
+
.replace(/ +/g, ' ')
|
|
33
|
+
.replace(/^\.+/, '')
|
|
34
|
+
.replace(/\/+/g, '/');
|
|
35
|
+
|
|
23
36
|
const sendLogToSupabase = async ({ file, uploadPath, status, message }) => {
|
|
24
37
|
const { error } = await supabase.from('upload_logs').insert([
|
|
25
38
|
{
|
|
@@ -37,9 +50,16 @@ const sendLogToSupabase = async ({ file, uploadPath, status, message }) => {
|
|
|
37
50
|
|
|
38
51
|
const checkCredentials = async () => {
|
|
39
52
|
if (!supabaseUrl || !supabaseKey || !bucket) {
|
|
40
|
-
console.error(
|
|
53
|
+
console.error(
|
|
54
|
+
'⚠️ Missing Supabase credentials. Please set SUPABASE_URL, SUPABASE_KEY, and SUPABASE_BUCKET in your environment variables.',
|
|
55
|
+
);
|
|
41
56
|
writeLog('⚠️ Missing Supabase credentials.');
|
|
42
|
-
await sendLogToSupabase({
|
|
57
|
+
await sendLogToSupabase({
|
|
58
|
+
file: 'Error',
|
|
59
|
+
uploadPath: 'Error',
|
|
60
|
+
status: 'error',
|
|
61
|
+
message: 'Missing Supabase credentials.',
|
|
62
|
+
});
|
|
43
63
|
process.exit(1);
|
|
44
64
|
}
|
|
45
65
|
|
|
@@ -48,13 +68,23 @@ const checkCredentials = async () => {
|
|
|
48
68
|
if (error) {
|
|
49
69
|
console.error('⚠️ Error connecting to Supabase:', error.message);
|
|
50
70
|
writeLog(`⚠️ Error connecting to Supabase: ${error.message}`);
|
|
51
|
-
await sendLogToSupabase({
|
|
71
|
+
await sendLogToSupabase({
|
|
72
|
+
file: 'Error',
|
|
73
|
+
uploadPath: 'Error',
|
|
74
|
+
status: 'error',
|
|
75
|
+
message: error.message,
|
|
76
|
+
});
|
|
52
77
|
process.exit(1);
|
|
53
78
|
}
|
|
54
79
|
} catch (err) {
|
|
55
80
|
console.error('⚠️ Error:', err.message);
|
|
56
81
|
writeLog(`⚠️ Error: ${err.message}`);
|
|
57
|
-
await sendLogToSupabase({
|
|
82
|
+
await sendLogToSupabase({
|
|
83
|
+
file: 'Error',
|
|
84
|
+
uploadPath: 'Error',
|
|
85
|
+
status: 'error',
|
|
86
|
+
message: err.message,
|
|
87
|
+
});
|
|
58
88
|
process.exit(1);
|
|
59
89
|
}
|
|
60
90
|
};
|
|
@@ -64,11 +94,18 @@ await checkCredentials();
|
|
|
64
94
|
const fileExistsInBucket = async (pathInBucket) => {
|
|
65
95
|
const dir = path.dirname(pathInBucket);
|
|
66
96
|
const filename = path.basename(pathInBucket);
|
|
67
|
-
const { data, error } = await supabase.storage
|
|
97
|
+
const { data, error } = await supabase.storage
|
|
98
|
+
.from(bucket)
|
|
99
|
+
.list(dir === '.' ? '' : dir, { limit: 1000 });
|
|
68
100
|
if (error) {
|
|
69
101
|
console.error(`⚠️ Could not verify duplicate: ${error.message}`);
|
|
70
102
|
writeLog(`⚠️ Could not verify duplicate: ${error.message}`);
|
|
71
|
-
await sendLogToSupabase({
|
|
103
|
+
await sendLogToSupabase({
|
|
104
|
+
file: 'Error',
|
|
105
|
+
uploadPath: 'Error',
|
|
106
|
+
status: 'error',
|
|
107
|
+
message: error.message,
|
|
108
|
+
});
|
|
72
109
|
return false;
|
|
73
110
|
}
|
|
74
111
|
return data.some((file) => file.name === filename);
|
|
@@ -76,8 +113,27 @@ const fileExistsInBucket = async (pathInBucket) => {
|
|
|
76
113
|
|
|
77
114
|
const logFilePath = path.resolve(process.cwd(), 'upload.log');
|
|
78
115
|
const writeLog = (message) => {
|
|
79
|
-
|
|
80
|
-
|
|
116
|
+
try {
|
|
117
|
+
const timestamp = new Date().toISOString();
|
|
118
|
+
fs.appendFileSync(logFilePath, `[${timestamp}] ${message}\n`);
|
|
119
|
+
} catch (error) {
|
|
120
|
+
console.error(`❌ Error writing to log file: ${err.message}`);
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
const getProcessedPaths = () => {
|
|
125
|
+
const lines = fs.existsSync(logFilePath)
|
|
126
|
+
? fs.readFileSync(logFilePath, 'utf-8').split('\n')
|
|
127
|
+
: [];
|
|
128
|
+
const processed = new Set();
|
|
129
|
+
for (const line of lines) {
|
|
130
|
+
const match = line.match(/(SUCCESS|SKIPPED): .*? -> (.+)/);
|
|
131
|
+
if (match) {
|
|
132
|
+
const [, , path] = match;
|
|
133
|
+
processed.add(path.trim());
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
return processed;
|
|
81
137
|
};
|
|
82
138
|
|
|
83
139
|
// const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
@@ -85,13 +141,17 @@ const writeLog = (message) => {
|
|
|
85
141
|
program
|
|
86
142
|
.name('supabase-uploader')
|
|
87
143
|
.description('CLI to upload folders from a base path to Supabase Storage')
|
|
144
|
+
.version(version)
|
|
88
145
|
.option('-p, --prefix <prefix>', 'Prefix path in bucket', '')
|
|
89
146
|
.action(async (options) => {
|
|
90
147
|
if (!basePath || !sources || sources.length === 0) {
|
|
91
|
-
console.error(
|
|
148
|
+
console.error(
|
|
149
|
+
'⚠️ UPLOAD_BASE_PATH or UPLOAD_SOURCES not defined in environment variables.',
|
|
150
|
+
);
|
|
92
151
|
process.exit(1);
|
|
93
152
|
}
|
|
94
153
|
|
|
154
|
+
const processedPaths = getProcessedPaths();
|
|
95
155
|
let globalSuccess = 0;
|
|
96
156
|
let globalFailure = 0;
|
|
97
157
|
|
|
@@ -101,15 +161,39 @@ program
|
|
|
101
161
|
|
|
102
162
|
try {
|
|
103
163
|
const stats = fs.statSync(sourcePath);
|
|
104
|
-
const files = stats.isDirectory()
|
|
164
|
+
const files = stats.isDirectory()
|
|
165
|
+
? await globby([`${sourcePath}/**/*`], { onlyFiles: true })
|
|
166
|
+
: [sourcePath];
|
|
105
167
|
|
|
106
168
|
let successCount = 0;
|
|
107
169
|
let failureCount = 0;
|
|
108
170
|
|
|
109
171
|
for (const file of files) {
|
|
110
172
|
const content = fs.readFileSync(file);
|
|
111
|
-
const
|
|
112
|
-
|
|
173
|
+
const relativePathRaw = path
|
|
174
|
+
.relative(basePath, file)
|
|
175
|
+
.replace(/^[\\/]+/, '')
|
|
176
|
+
.replace(/\\/g, '/');
|
|
177
|
+
const uploadPathRaw = options.prefix
|
|
178
|
+
? path.posix.join(options.prefix, relativePathRaw)
|
|
179
|
+
: relativePathRaw;
|
|
180
|
+
const uploadPath = sanitizePath(uploadPathRaw);
|
|
181
|
+
|
|
182
|
+
if (uploadPath !== uploadPathRaw) {
|
|
183
|
+
writeLog(`SANITIZED: ${uploadPathRaw} → ${uploadPath}`);
|
|
184
|
+
await sendLogToSupabase({
|
|
185
|
+
file,
|
|
186
|
+
uploadPath: uploadPathRaw,
|
|
187
|
+
status: 'sanitized',
|
|
188
|
+
message: `Sanitized to ${uploadPath}`,
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (processedPaths.has(uploadPath)) {
|
|
193
|
+
ora().info(`⏭️ Already processed (log): ${file}`);
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
|
|
113
197
|
const contentType = mime.lookup(file) || 'application/octet-stream';
|
|
114
198
|
|
|
115
199
|
const spinner = ora(`Checking ${file}...`).start();
|
|
@@ -118,30 +202,52 @@ program
|
|
|
118
202
|
if (exists) {
|
|
119
203
|
spinner.info(`⏭️ Skipped (already exists): ${file}`);
|
|
120
204
|
writeLog(`SKIPPED: ${file} -> ${uploadPath}`);
|
|
121
|
-
await sendLogToSupabase({
|
|
205
|
+
await sendLogToSupabase({
|
|
206
|
+
file,
|
|
207
|
+
uploadPath,
|
|
208
|
+
status: 'skipped',
|
|
209
|
+
message: 'Already exists in bucket',
|
|
210
|
+
});
|
|
122
211
|
continue;
|
|
123
212
|
}
|
|
124
213
|
|
|
125
214
|
try {
|
|
126
215
|
// await delay(5000); // TODO: Remove this delay before production
|
|
127
216
|
|
|
128
|
-
const { error } = await supabase.storage
|
|
217
|
+
const { error } = await supabase.storage
|
|
218
|
+
.from(bucket)
|
|
219
|
+
.upload(uploadPath, content, {
|
|
129
220
|
upsert: true,
|
|
130
221
|
contentType,
|
|
222
|
+
metadata: {
|
|
223
|
+
originalName: path.basename(file),
|
|
224
|
+
},
|
|
131
225
|
});
|
|
132
226
|
|
|
133
227
|
if (error) {
|
|
134
228
|
failureCount++;
|
|
135
229
|
globalFailure++;
|
|
136
|
-
spinner.fail(
|
|
230
|
+
spinner.fail(
|
|
231
|
+
`❌ Failed to upload ${file}: ${JSON.stringify(error, null, 2)}`,
|
|
232
|
+
);
|
|
137
233
|
writeLog(`ERROR: ${file} -> ${uploadPath} | ${error.message}`);
|
|
138
|
-
await sendLogToSupabase({
|
|
234
|
+
await sendLogToSupabase({
|
|
235
|
+
file,
|
|
236
|
+
uploadPath,
|
|
237
|
+
status: 'error',
|
|
238
|
+
message: error.message,
|
|
239
|
+
});
|
|
139
240
|
} else {
|
|
140
241
|
successCount++;
|
|
141
242
|
globalSuccess++;
|
|
142
243
|
spinner.succeed(`✅ Uploaded ${file} -> ${uploadPath}`);
|
|
143
244
|
writeLog(`SUCCESS: ${file} -> ${uploadPath}`);
|
|
144
|
-
await sendLogToSupabase({
|
|
245
|
+
await sendLogToSupabase({
|
|
246
|
+
file,
|
|
247
|
+
uploadPath,
|
|
248
|
+
status: 'success',
|
|
249
|
+
message: 'Uploaded successfully',
|
|
250
|
+
});
|
|
145
251
|
}
|
|
146
252
|
} catch (err) {
|
|
147
253
|
spinner.fail(`❌ Error uploading ${file}: ${err.message}`);
|
|
@@ -152,14 +258,23 @@ program
|
|
|
152
258
|
console.log(`\n📦 Upload Summary:`);
|
|
153
259
|
console.log(` ✅ Successfully uploaded files: ${successCount}`);
|
|
154
260
|
console.log(` ❌ Files with errors: ${failureCount}`);
|
|
155
|
-
console.log(
|
|
261
|
+
console.log(
|
|
262
|
+
` ⏭️ Files skipped (already exist): ${files.length - successCount - failureCount}`,
|
|
263
|
+
);
|
|
156
264
|
console.log(` 📜 Log file: ${logFilePath} \n`);
|
|
157
265
|
|
|
158
|
-
writeLog(
|
|
266
|
+
writeLog(
|
|
267
|
+
`📦 Upload Summary for folder ${folder}: Success: ${successCount}, Errors: ${failureCount}, Skipped: ${files.length - successCount - failureCount}`,
|
|
268
|
+
);
|
|
159
269
|
} catch (err) {
|
|
160
270
|
console.error(`⚠️ Error processing folder ${folder}:`, err.message);
|
|
161
271
|
writeLog(`⚠️ Error processing folder ${folder}: ${err.message}`);
|
|
162
|
-
await sendLogToSupabase({
|
|
272
|
+
await sendLogToSupabase({
|
|
273
|
+
file: folder,
|
|
274
|
+
uploadPath: folder,
|
|
275
|
+
status: 'error',
|
|
276
|
+
message: err.message,
|
|
277
|
+
});
|
|
163
278
|
}
|
|
164
279
|
}
|
|
165
280
|
|