@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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/index.js +136 -21
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@arela/uploader",
3
- "version": "0.0.5",
3
+ "version": "0.0.7",
4
4
  "description": "CLI to upload files/directories to Arela",
5
5
  "bin": {
6
6
  "arela": "./src/index.js"
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('|').map((s) => s.trim()).filter(Boolean);
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('⚠️ Missing Supabase credentials. Please set SUPABASE_URL, SUPABASE_KEY, and SUPABASE_BUCKET in your environment variables.');
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({ file: 'Error', uploadPath: 'Error', status: 'error', message: 'Missing Supabase credentials.' });
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({ file: 'Error', uploadPath: 'Error', status: 'error', message: error.message });
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({ file: 'Error', uploadPath: 'Error', status: 'error', message: err.message });
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.from(bucket).list(dir === '.' ? '' : dir, { limit: 1000 });
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({ file: 'Error', uploadPath: 'Error', status: 'error', message: error.message });
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
- const timestamp = new Date().toISOString();
80
- fs.appendFileSync(logFilePath, `[${timestamp}] ${message}\n`);
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('⚠️ UPLOAD_BASE_PATH or UPLOAD_SOURCES not defined in environment variables.');
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() ? await globby([`${sourcePath}/**/*`], { onlyFiles: true }) : [sourcePath];
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 relativePath = path.relative(basePath, file).replace(/^[\\/]+/, '').replace(/\\/g, '/');
112
- const uploadPath = options.prefix ? path.posix.join(options.prefix, relativePath) : relativePath;
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({ file, uploadPath, status: 'skipped', message: 'Already exists in bucket' });
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.from(bucket).upload(uploadPath, content, {
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(`❌ Failed to upload ${file}: ${JSON.stringify(error, null, 2)}`);
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({ file, uploadPath, status: 'error', message: error.message });
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({ file, uploadPath, status: 'success', message: 'Uploaded successfully' });
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(` ⏭️ Files skipped (already exist): ${files.length - successCount - failureCount}`);
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(`📦 Upload Summary for folder ${folder}: Success: ${successCount}, Errors: ${failureCount}, Skipped: ${files.length - successCount - failureCount}`);
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({ file: folder, uploadPath: folder, status: 'error', message: err.message });
272
+ await sendLogToSupabase({
273
+ file: folder,
274
+ uploadPath: folder,
275
+ status: 'error',
276
+ message: err.message,
277
+ });
163
278
  }
164
279
  }
165
280