aspect-sync 0.0.1

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 ADDED
@@ -0,0 +1,278 @@
1
+ # Aspect Data Sync
2
+
3
+ A CLI tool to sync files from external services (via rclone) to the Aspect platform.
4
+
5
+ ## Features
6
+
7
+ - **Rclone Integration**: Sync files from any rclone-supported remote (Dropbox, Google Drive, S3, etc.)
8
+ - **Directory Structure Preservation**: Maintains folder hierarchy from source to destination
9
+ - **Multipart Upload**: Efficient chunked uploads with retry logic (20MB chunks)
10
+ - **Dynamic CLI Progress**: Real-time upload progress display with in-place updates
11
+ - **Concurrent Uploads**: Configurable parallel chunk uploads for optimal performance
12
+ - **Automatic Cleanup**: Removes local files after successful upload
13
+ - **Retry Logic**: Automatic retry for failed uploads (part-level and file-level)
14
+
15
+ ## Prerequisites
16
+
17
+ 1. **Node.js 22+** installed
18
+ 2. **rclone** installed and configured
19
+ - Install: https://rclone.org/install/
20
+ - Configure your remote: `rclone config`
21
+ 3. **Aspect API Key** for authentication
22
+
23
+ ## Configuring Rclone
24
+
25
+ Before using this tool, you must configure rclone with your cloud storage provider.
26
+
27
+ ### Initial Setup
28
+
29
+ 1. Run the rclone configuration wizard:
30
+ ```bash
31
+ rclone config
32
+ ```
33
+
34
+ 2. Choose `n` for "New remote"
35
+
36
+ 3. Give it a name (e.g., `dropbox`, `gdrive`, `my-s3`)
37
+ - **This name is what you'll use for the `--remote` parameter**
38
+ - Use only letters, numbers, hyphens, and underscores
39
+ - Don't include special characters or colons
40
+
41
+ 4. Select your storage provider from the list (Dropbox, Google Drive, S3, etc.)
42
+
43
+ 5. Follow the prompts to authenticate with your cloud provider
44
+
45
+ ### Verify Configuration
46
+
47
+ Check your configured remotes:
48
+ ```bash
49
+ rclone listremotes
50
+ ```
51
+
52
+ Output example:
53
+ ```
54
+ dropbox:
55
+ gdrive:
56
+ my-s3:
57
+ ```
58
+
59
+ **Important:** The remote names are shown **with a colon** (`:`) but you use them **without the colon** in the `--remote` parameter.
60
+
61
+ Examples:
62
+ - If you see `dropbox:` → use `--remote dropbox`
63
+ - If you see `my-dropbox:` → use `--remote my-dropbox`
64
+ - If you see `gdrive:` → use `--remote gdrive`
65
+
66
+ ### Test Your Remote
67
+
68
+ Before syncing, test that your remote works:
69
+ ```bash
70
+ # List files in the root of your remote
71
+ rclone ls dropbox:
72
+
73
+ # List files in a specific folder
74
+ rclone ls dropbox:my-folder
75
+ ```
76
+
77
+ This should list files in your cloud storage. If you see files listed, your remote is configured correctly!
78
+
79
+ ## Installation
80
+
81
+ Install globally via npm:
82
+
83
+ ```bash
84
+ npm install -g aspect-sync
85
+ ```
86
+
87
+ Or use directly with npx (no installation required):
88
+
89
+ ```bash
90
+ npx aspect-sync --help
91
+ ```
92
+
93
+ ## Usage
94
+
95
+ ### Basic Command
96
+
97
+ If installed globally:
98
+ ```bash
99
+ aspect-sync \
100
+ --remote dropbox \
101
+ --path /my-videos \
102
+ --directory-id <aspect-directory-id> \
103
+ --project-id <aspect-project-id> \
104
+ --api-key <your-api-key>
105
+ ```
106
+
107
+ Or with npx (no installation):
108
+ ```bash
109
+ npx aspect-sync \
110
+ --remote dropbox \
111
+ --path /my-videos \
112
+ --directory-id <aspect-directory-id> \
113
+ --project-id <aspect-project-id> \
114
+ --api-key <your-api-key>
115
+ ```
116
+
117
+ ### Command Options
118
+
119
+ | Option | Description | Required | Default |
120
+ |--------|-------------|----------|---------|
121
+ | `--remote <name>` | rclone remote name from your rclone config (without colon). Use `rclone listremotes` to see your configured remotes. | Yes | - |
122
+ | `--path <path>` | Remote path to sync from | Yes | - |
123
+ | `--directory-id <id>` | Aspect directory ID to upload to | Yes | - |
124
+ | `--project-id <id>` | Aspect project ID | Yes | - |
125
+ | `--api-key <key>` | Aspect API key (or set `ASPECT_API_KEY` env var) | Yes | - |
126
+ | `--api-url <url>` | Aspect API URL (or set `ASPECT_API_URL` env var) | No | `http://localhost:8000` |
127
+ | `--concurrent <number>` | Max concurrent chunk uploads | No | `4` |
128
+ | `--keep-local` | Keep local files after upload (for debugging) | No | `false` |
129
+ | `--temp-dir <path>` | Local temporary directory for synced files | No | `$TMPDIR/aspect-sync` |
130
+
131
+ ### Environment Variables
132
+
133
+ You can set API credentials via environment variables:
134
+
135
+ ```bash
136
+ export ASPECT_API_KEY=your-api-key
137
+ export ASPECT_API_URL=https://api.aspect.inc
138
+
139
+ aspect-sync \
140
+ --remote dropbox \
141
+ --path /my-videos \
142
+ --directory-id <directory-id> \
143
+ --project-id <project-id>
144
+ ```
145
+
146
+ ## Examples
147
+
148
+ ### Sync from Dropbox
149
+
150
+ ```bash
151
+ aspect-sync \
152
+ --remote dropbox \
153
+ --path /Camera Uploads \
154
+ --directory-id abc-123-def \
155
+ --project-id xyz-789-uvw \
156
+ --api-key your-api-key
157
+ ```
158
+
159
+ ### Sync from Google Drive with Custom Concurrency
160
+
161
+ ```bash
162
+ aspect-sync \
163
+ --remote gdrive \
164
+ --path /Videos/2024 \
165
+ --directory-id abc-123-def \
166
+ --project-id xyz-789-uvw \
167
+ --api-key your-api-key \
168
+ --concurrent 8
169
+ ```
170
+
171
+ ### Sync from S3 Bucket
172
+
173
+ ```bash
174
+ aspect-sync \
175
+ --remote s3 \
176
+ --path mybucket/videos \
177
+ --directory-id abc-123-def \
178
+ --project-id xyz-789-uvw \
179
+ --api-key your-api-key
180
+ ```
181
+
182
+ ## How It Works
183
+
184
+ 1. **Rclone Sync**: Downloads files from the remote to a local temp directory
185
+ 2. **Directory Creation**: Creates matching folder structure in Aspect
186
+ 3. **File Upload**: Uploads each file using multipart upload (20MB chunks)
187
+ 4. **Progress Display**: Shows real-time upload progress in the CLI
188
+ 5. **Cleanup**: Deletes local files after successful upload (unless `--keep-local` is set)
189
+
190
+ ## CLI Progress Display
191
+
192
+ The tool shows dynamic, in-place progress updates:
193
+
194
+ ```
195
+ ✓ video1.mp4 (100%)
196
+ ✓ video2.mp4 (100%)
197
+ ↑ video3.mp4 (47%)
198
+ ↑ video4.mp4 (23%)
199
+
200
+ Total: 10 | Success: 2 | Failed: 0 | In Progress: 2 | Queued: 6 | Speed: 45.3 Mb/s | Time remaining: 2m 15s
201
+ ```
202
+
203
+ - `✓` = Successfully uploaded
204
+ - `✗` = Failed upload
205
+ - `↑` = Currently uploading
206
+
207
+ Only files that are actively uploading are shown (not the entire queue).
208
+
209
+ ## Error Handling
210
+
211
+ ### Retry Logic
212
+
213
+ - **Part-level retries**: Each 20MB chunk is retried up to 3 times on failure
214
+ - **File-level retries**: Each file upload is retried up to 3 times on failure
215
+ - **Exponential backoff**: Delays increase exponentially between retries
216
+
217
+ ### Common Errors
218
+
219
+ **rclone not found:**
220
+ ```
221
+ Error: rclone is not installed or not in PATH
222
+ ```
223
+ Solution: Install rclone from https://rclone.org/install/
224
+
225
+ **Authentication failed:**
226
+ ```
227
+ Error: 401 Unauthorized
228
+ ```
229
+ Solution: Check your `--api-key` is correct
230
+
231
+ **Directory not found:**
232
+ ```
233
+ Error: 404 Not Found - Directory not found
234
+ ```
235
+ Solution: Verify `--directory-id` exists and you have access
236
+
237
+ ## Troubleshooting
238
+
239
+ ### Keep local files for debugging
240
+
241
+ Use `--keep-local` to prevent deletion of synced files:
242
+
243
+ ```bash
244
+ aspect-sync \
245
+ --remote dropbox \
246
+ --path /test \
247
+ --directory-id abc-123 \
248
+ --project-id xyz-789 \
249
+ --api-key your-key \
250
+ --keep-local
251
+ ```
252
+
253
+ ### Increase concurrency
254
+
255
+ If you have high bandwidth, increase `--concurrent`:
256
+
257
+ ```bash
258
+ aspect-sync \
259
+ --remote dropbox \
260
+ --path /test \
261
+ --directory-id abc-123 \
262
+ --project-id xyz-789 \
263
+ --api-key your-key \
264
+ --concurrent 16
265
+ ```
266
+
267
+ ### Check rclone configuration
268
+
269
+ Verify your rclone remote is configured:
270
+
271
+ ```bash
272
+ rclone listremotes
273
+ rclone ls dropbox:
274
+ ```
275
+
276
+ ## License
277
+
278
+ MIT
@@ -0,0 +1,8 @@
1
+ declare class CliDisplay {
2
+ #private;
3
+ start(): void;
4
+ stop(): void;
5
+ }
6
+ export declare const cliDisplay: CliDisplay;
7
+ export {};
8
+ //# sourceMappingURL=cliDisplay.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cliDisplay.d.ts","sourceRoot":"","sources":["../src/cliDisplay.ts"],"names":[],"mappings":"AAKA,cAAM,UAAU;;IAOd,KAAK,IAAI,IAAI;IAkBb,IAAI,IAAI,IAAI;CAoGb;AAED,eAAO,MAAM,UAAU,YAAmB,CAAA"}
@@ -0,0 +1,101 @@
1
+ import logUpdate from "log-update";
2
+ import chalk from "chalk";
3
+ import { statusTracker } from "./statusTracker.js";
4
+ import { UploadStatus } from "./types.js";
5
+ class CliDisplay {
6
+ #completedLines = [];
7
+ #completedFileIds = new Set();
8
+ #updateInterval = null;
9
+ #statsInterval = null;
10
+ #isRunning = false;
11
+ start() {
12
+ if (this.#isRunning)
13
+ return;
14
+ this.#isRunning = true;
15
+ // Update display every 100ms for smooth progress bars
16
+ this.#updateInterval = setInterval(() => {
17
+ this.#render();
18
+ }, 100);
19
+ // Update stats every 1 second for stable speed/time calculations
20
+ this.#statsInterval = setInterval(() => {
21
+ statusTracker.updateUploadStats();
22
+ }, 1000);
23
+ // Calculate stats immediately on start
24
+ statusTracker.updateUploadStats();
25
+ }
26
+ stop() {
27
+ if (!this.#isRunning)
28
+ return;
29
+ this.#isRunning = false;
30
+ if (this.#updateInterval) {
31
+ clearInterval(this.#updateInterval);
32
+ this.#updateInterval = null;
33
+ }
34
+ if (this.#statsInterval) {
35
+ clearInterval(this.#statsInterval);
36
+ this.#statsInterval = null;
37
+ }
38
+ // Update stats one final time for accurate final display
39
+ statusTracker.updateUploadStats();
40
+ this.#render();
41
+ logUpdate.done();
42
+ }
43
+ #render() {
44
+ const inProgressFiles = statusTracker.getInProgressFiles();
45
+ const allFiles = statusTracker.getAllUploadFiles();
46
+ const totals = statusTracker.getUploadTotals();
47
+ // Check for newly completed/failed files since last render
48
+ for (const file of allFiles) {
49
+ if ((file.uploadStatus === UploadStatus.SUCCESS || file.uploadStatus === UploadStatus.FAILED) &&
50
+ !this.#completedFileIds.has(file.fileId)) {
51
+ this.#addCompletedLine(file);
52
+ this.#completedFileIds.add(file.fileId);
53
+ }
54
+ }
55
+ // Build the output
56
+ const lines = [];
57
+ // Add all completed lines (these are frozen)
58
+ lines.push(...this.#completedLines);
59
+ // Add in-progress files (these update dynamically)
60
+ for (const file of inProgressFiles) {
61
+ lines.push(this.#formatInProgressFile(file));
62
+ }
63
+ // Add summary line
64
+ lines.push("");
65
+ lines.push(this.#formatSummary(totals));
66
+ // Update the display
67
+ logUpdate(lines.join("\n"));
68
+ }
69
+ #addCompletedLine(file) {
70
+ if (file.uploadStatus === UploadStatus.SUCCESS) {
71
+ this.#completedLines.push(`${chalk.green("✓")} ${file.fileName} ${chalk.green("(100%)")}`);
72
+ }
73
+ else if (file.uploadStatus === UploadStatus.FAILED) {
74
+ this.#completedLines.push(`${chalk.red("✗")} ${file.fileName} ${chalk.red(`(Failed: ${file.errorMessage || "Unknown error"})`)}`);
75
+ }
76
+ }
77
+ #formatInProgressFile(file) {
78
+ const percentage = file.totalBytesToUpload > 0
79
+ ? Math.floor((file.bytesUploaded / file.totalBytesToUpload) * 100)
80
+ : 0;
81
+ return `${chalk.blue("↑")} ${file.fileName} ${chalk.blue(`(${percentage}%)`)}`;
82
+ }
83
+ #formatSummary(totals) {
84
+ // Stats are updated by the 1-second interval, just read cached values
85
+ const stats = statusTracker.uploadStats;
86
+ const parts = [
87
+ `Total: ${totals.totalFileCount}`,
88
+ chalk.green(`Success: ${totals.successFileCount}`),
89
+ chalk.red(`Failed: ${totals.failedFileCount}`),
90
+ chalk.blue(`In Progress: ${totals.inProgressFileCount}`),
91
+ chalk.gray(`Queued: ${totals.queuedFileCount}`),
92
+ ];
93
+ const statsLine = stats.formattedSpeed ? [
94
+ chalk.cyan(`Speed: ${stats.formattedSpeed}`),
95
+ chalk.magenta(`Time remaining: ${stats.formattedTime}`),
96
+ ] : [];
97
+ return `${parts.join(" | ")}${statsLine.length > 0 ? ` | ${statsLine.join(" | ")}` : ""}`;
98
+ }
99
+ }
100
+ export const cliDisplay = new CliDisplay();
101
+ //# sourceMappingURL=cliDisplay.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cliDisplay.js","sourceRoot":"","sources":["../src/cliDisplay.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ export {};
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":""}
package/dist/index.js ADDED
@@ -0,0 +1,55 @@
1
+ #!/usr/bin/env node
2
+ import { Command } from "commander";
3
+ import * as path from "path";
4
+ import * as os from "os";
5
+ import { SyncOrchestrator } from "./sync.js";
6
+ const program = new Command();
7
+ program
8
+ .name("aspect-sync")
9
+ .description("Sync files from external services to Aspect via rclone")
10
+ .version("0.1.0");
11
+ program
12
+ .requiredOption("--remote <remote>", "rclone remote name (e.g., dropbox)")
13
+ .requiredOption("--path <path>", "remote path to sync from")
14
+ .requiredOption("--directory-id <id>", "Aspect directory ID to upload to")
15
+ .requiredOption("--project-id <id>", "Aspect project ID")
16
+ .option("--api-url <url>", "Aspect API URL", process.env.ASPECT_API_URL || "https://api.aspect.inc")
17
+ .option("--api-key <key>", "Aspect API key", process.env.ASPECT_API_KEY || "")
18
+ .option("--concurrent <number>", "Maximum concurrent chunk uploads", "4")
19
+ .option("--keep-local", "Keep local files after upload (for debugging)", false)
20
+ .option("--temp-dir <path>", "Local temporary directory for synced files", path.join(os.homedir(), ".aspect", "sync"))
21
+ .action(async (options) => {
22
+ // Validate required options
23
+ if (!options.apiKey) {
24
+ console.error("Error: --api-key is required (or set ASPECT_API_KEY env variable)");
25
+ process.exit(1);
26
+ }
27
+ // Build config
28
+ const config = {
29
+ remote: options.remote,
30
+ remotePath: options.path,
31
+ directoryId: options.directoryId,
32
+ projectId: options.projectId,
33
+ apiUrl: options.apiUrl,
34
+ apiKey: options.apiKey,
35
+ maxConcurrent: parseInt(options.concurrent, 10),
36
+ keepLocal: options.keepLocal,
37
+ localTempDir: options.tempDir,
38
+ };
39
+ // Validate config
40
+ if (isNaN(config.maxConcurrent) || config.maxConcurrent < 1) {
41
+ console.error("Error: --concurrent must be a positive integer");
42
+ process.exit(1);
43
+ }
44
+ try {
45
+ const orchestrator = new SyncOrchestrator(config);
46
+ await orchestrator.run();
47
+ process.exit(0);
48
+ }
49
+ catch (error) {
50
+ console.error("Fatal error:", error);
51
+ process.exit(1);
52
+ }
53
+ });
54
+ program.parse();
55
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAEA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAA;AACnC,OAAO,KAAK,IAAI,MAAM,MAAM,CAAA;AAC5B,OAAO,KAAK,EAAE,MAAM,IAAI,CAAA;AACxB,OAAO,EAAE,gBAAgB,EAAE,MAAM,WAAW,CAAA;AAG5C,MAAM,OAAO,GAAG,IAAI,OAAO,EAAE,CAAA;AAE7B,OAAO;KACJ,IAAI,CAAC,aAAa,CAAC;KACnB,WAAW,CAAC,wDAAwD,CAAC;KACrE,OAAO,CAAC,OAAO,CAAC,CAAA;AAEnB,OAAO;KACJ,cAAc,CAAC,mBAAmB,EAAE,oCAAoC,CAAC;KACzE,cAAc,CAAC,eAAe,EAAE,0BAA0B,CAAC;KAC3D,cAAc,CAAC,qBAAqB,EAAE,kCAAkC,CAAC;KACzE,cAAc,CAAC,mBAAmB,EAAE,mBAAmB,CAAC;KACxD,MAAM,CAAC,iBAAiB,EAAE,gBAAgB,EAAE,OAAO,CAAC,GAAG,CAAC,cAAc,IAAI,wBAAwB,CAAC;KACnG,MAAM,CAAC,iBAAiB,EAAE,gBAAgB,EAAE,OAAO,CAAC,GAAG,CAAC,cAAc,IAAI,EAAE,CAAC;KAC7E,MAAM,CAAC,uBAAuB,EAAE,kCAAkC,EAAE,GAAG,CAAC;KACxE,MAAM,CAAC,cAAc,EAAE,+CAA+C,EAAE,KAAK,CAAC;KAC9E,MAAM,CAAC,mBAAmB,EAAE,4CAA4C,EAAE,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,OAAO,EAAE,EAAE,SAAS,EAAE,MAAM,CAAC,CAAC;KACrH,MAAM,CAAC,KAAK,EAAE,OAAO,EAAE,EAAE;IACxB,4BAA4B;IAC5B,IAAI,CAAC,OAAO,CAAC,MAAM,EAAE,CAAC;QACpB,OAAO,CAAC,KAAK,CAAC,mEAAmE,CAAC,CAAA;QAClF,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAA;IACjB,CAAC;IAED,eAAe;IACf,MAAM,MAAM,GAAe;QACzB,MAAM,EAAE,OAAO,CAAC,MAAM;QACtB,UAAU,EAAE,OAAO,CAAC,IAAI;QACxB,WAAW,EAAE,OAAO,CAAC,WAAW;QAChC,SAAS,EAAE,OAAO,CAAC,SAAS;QAC5B,MAAM,EAAE,OAAO,CAAC,MAAM;QACtB,MAAM,EAAE,OAAO,CAAC,MAAM;QACtB,aAAa,EAAE,QAAQ,CAAC,OAAO,CAAC,UAAU,EAAE,EAAE,CAAC;QAC/C,SAAS,EAAE,OAAO,CAAC,SAAS;QAC5B,YAAY,EAAE,OAAO,CAAC,OAAO;KAC9B,CAAA;IAED,kBAAkB;IAClB,IAAI,KAAK,CAAC,MAAM,CAAC,aAAa,CAAC,IAAI,MAAM,CAAC,aAAa,GAAG,CAAC,EAAE,CAAC;QAC5D,OAAO,CAAC,KAAK,CAAC,gDAAgD,CAAC,CAAA;QAC/D,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAA;IACjB,CAAC;IAED,IAAI,CAAC;QACH,MAAM,YAAY,GAAG,IAAI,gBAAgB,CAAC,MAAM,CAAC,CAAA;QACjD,MAAM,YAAY,CAAC,GAAG,EAAE,CAAA;QACxB,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAA;IACjB,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,OAAO,CAAC,KAAK,CAAC,cAAc,EAAE,KAAK,CAAC,CAAA;QACpC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAA;IACjB,CAAC;AACH,CAAC,CAAC,CAAA;AAEJ,OAAO,CAAC,KAAK,EAAE,CAAA"}
@@ -0,0 +1,17 @@
1
+ export declare class RcloneError extends Error {
2
+ readonly stderr: string;
3
+ constructor(message: string, stderr: string);
4
+ }
5
+ export declare function checkRcloneInstalled(): Promise<void>;
6
+ export declare function syncFromRemote(remote: string, remotePath: string, localPath: string): Promise<void>;
7
+ export declare function listRemoteFiles(remote: string, remotePath: string): Promise<string[]>;
8
+ export interface LocalFileInfo {
9
+ relativePath: string;
10
+ absolutePath: string;
11
+ fileName: string;
12
+ size: number;
13
+ }
14
+ export declare function scanLocalDirectory(localPath: string): Promise<LocalFileInfo[]>;
15
+ export declare function deleteLocalFile(filePath: string): Promise<void>;
16
+ export declare function deleteEmptyDirectories(rootPath: string): Promise<void>;
17
+ //# sourceMappingURL=rclone.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rclone.d.ts","sourceRoot":"","sources":["../src/rclone.ts"],"names":[],"mappings":"AAIA,qBAAa,WAAY,SAAQ,KAAK;aACS,MAAM,EAAE,MAAM;gBAA/C,OAAO,EAAE,MAAM,EAAkB,MAAM,EAAE,MAAM;CAI5D;AAED,wBAAsB,oBAAoB,IAAI,OAAO,CAAC,IAAI,CAAC,CAoB1D;AAED,wBAAsB,cAAc,CAClC,MAAM,EAAE,MAAM,EACd,UAAU,EAAE,MAAM,EAClB,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,IAAI,CAAC,CAuDf;AAED,wBAAsB,eAAe,CACnC,MAAM,EAAE,MAAM,EACd,UAAU,EAAE,MAAM,GACjB,OAAO,CAAC,MAAM,EAAE,CAAC,CA0CnB;AAED,MAAM,WAAW,aAAa;IAC5B,YAAY,EAAE,MAAM,CAAA;IACpB,YAAY,EAAE,MAAM,CAAA;IACpB,QAAQ,EAAE,MAAM,CAAA;IAChB,IAAI,EAAE,MAAM,CAAA;CACb;AAED,wBAAsB,kBAAkB,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,EAAE,CAAC,CA0BpF;AAED,wBAAsB,eAAe,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAMrE;AAED,wBAAsB,sBAAsB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CA+B5E"}
package/dist/rclone.js ADDED
@@ -0,0 +1,164 @@
1
+ import { spawn } from "child_process";
2
+ import * as fs from "fs";
3
+ import * as path from "path";
4
+ export class RcloneError extends Error {
5
+ stderr;
6
+ constructor(message, stderr) {
7
+ super(message);
8
+ this.stderr = stderr;
9
+ this.name = "RcloneError";
10
+ }
11
+ }
12
+ export async function checkRcloneInstalled() {
13
+ return new Promise((resolve, reject) => {
14
+ const process = spawn("rclone", ["version"]);
15
+ process.on("error", () => {
16
+ reject(new Error("rclone is not installed or not in PATH. Please install rclone first: https://rclone.org/install/"));
17
+ });
18
+ process.on("close", (code) => {
19
+ if (code === 0) {
20
+ resolve();
21
+ }
22
+ else {
23
+ reject(new Error("rclone is not installed or not in PATH. Please install rclone first: https://rclone.org/install/"));
24
+ }
25
+ });
26
+ });
27
+ }
28
+ export async function syncFromRemote(remote, remotePath, localPath) {
29
+ const remoteSource = `${remote}:${remotePath}`;
30
+ console.log(`Syncing from ${remoteSource} to ${localPath}...`);
31
+ console.log("");
32
+ return new Promise((resolve, reject) => {
33
+ const rcloneProcess = spawn("rclone", [
34
+ "sync",
35
+ remoteSource,
36
+ localPath,
37
+ "--progress",
38
+ "--stats", "1s",
39
+ "--stats-one-line",
40
+ "-v"
41
+ ]);
42
+ let stderrOutput = "";
43
+ // Stream stdout to console in real-time
44
+ rcloneProcess.stdout.on("data", (data) => {
45
+ process.stdout.write(data);
46
+ });
47
+ // Capture stderr for error reporting
48
+ rcloneProcess.stderr.on("data", (data) => {
49
+ const text = data.toString();
50
+ stderrOutput += text;
51
+ process.stderr.write(data);
52
+ });
53
+ // Handle process completion
54
+ rcloneProcess.on("close", (code) => {
55
+ if (code === 0) {
56
+ console.log("");
57
+ console.log(`✓ Sync complete: ${remoteSource} -> ${localPath}`);
58
+ resolve();
59
+ }
60
+ else {
61
+ console.log("");
62
+ console.log(`✗ Failed to sync from ${remoteSource}`);
63
+ reject(new RcloneError(`rclone exited with code ${code}`, stderrOutput));
64
+ }
65
+ });
66
+ // Handle spawn errors (e.g., rclone not found)
67
+ rcloneProcess.on("error", (error) => {
68
+ reject(new RcloneError(`Failed to start rclone: ${error.message}`, ""));
69
+ });
70
+ });
71
+ }
72
+ export async function listRemoteFiles(remote, remotePath) {
73
+ const remoteSource = `${remote}:${remotePath}`;
74
+ return new Promise((resolve, reject) => {
75
+ const rcloneProcess = spawn("rclone", [
76
+ "lsf",
77
+ remoteSource,
78
+ "--recursive"
79
+ ]);
80
+ let stdoutOutput = "";
81
+ let stderrOutput = "";
82
+ rcloneProcess.stdout.on("data", (data) => {
83
+ stdoutOutput += data.toString();
84
+ });
85
+ rcloneProcess.stderr.on("data", (data) => {
86
+ stderrOutput += data.toString();
87
+ });
88
+ rcloneProcess.on("close", (code) => {
89
+ if (code === 0) {
90
+ const files = stdoutOutput
91
+ .split("\n")
92
+ .filter(line => line.trim() !== "" && !line.endsWith("/"));
93
+ resolve(files);
94
+ }
95
+ else {
96
+ reject(new RcloneError(`Failed to list files from ${remoteSource}`, stderrOutput));
97
+ }
98
+ });
99
+ rcloneProcess.on("error", (error) => {
100
+ reject(new RcloneError(`Failed to start rclone: ${error.message}`, ""));
101
+ });
102
+ });
103
+ }
104
+ export async function scanLocalDirectory(localPath) {
105
+ const files = [];
106
+ async function scanDir(currentPath, relativePath = "") {
107
+ const entries = await fs.promises.readdir(currentPath, { withFileTypes: true });
108
+ for (const entry of entries) {
109
+ const entryPath = path.join(currentPath, entry.name);
110
+ const entryRelativePath = relativePath ? path.join(relativePath, entry.name) : entry.name;
111
+ if (entry.isDirectory()) {
112
+ await scanDir(entryPath, entryRelativePath);
113
+ }
114
+ else if (entry.isFile()) {
115
+ const stats = await fs.promises.stat(entryPath);
116
+ files.push({
117
+ relativePath: entryRelativePath,
118
+ absolutePath: entryPath,
119
+ fileName: entry.name,
120
+ size: stats.size,
121
+ });
122
+ }
123
+ }
124
+ }
125
+ await scanDir(localPath);
126
+ return files;
127
+ }
128
+ export async function deleteLocalFile(filePath) {
129
+ try {
130
+ await fs.promises.unlink(filePath);
131
+ }
132
+ catch (error) {
133
+ console.error(`Failed to delete local file ${filePath}:`, error.message);
134
+ }
135
+ }
136
+ export async function deleteEmptyDirectories(rootPath) {
137
+ async function deleteEmptyDirs(currentPath) {
138
+ const entries = await fs.promises.readdir(currentPath, { withFileTypes: true });
139
+ if (entries.length === 0) {
140
+ await fs.promises.rmdir(currentPath);
141
+ return true;
142
+ }
143
+ let isEmpty = true;
144
+ for (const entry of entries) {
145
+ if (entry.isDirectory()) {
146
+ const entryPath = path.join(currentPath, entry.name);
147
+ const wasDeleted = await deleteEmptyDirs(entryPath);
148
+ if (!wasDeleted) {
149
+ isEmpty = false;
150
+ }
151
+ }
152
+ else {
153
+ isEmpty = false;
154
+ }
155
+ }
156
+ if (isEmpty && currentPath !== rootPath) {
157
+ await fs.promises.rmdir(currentPath);
158
+ return true;
159
+ }
160
+ return false;
161
+ }
162
+ await deleteEmptyDirs(rootPath);
163
+ }
164
+ //# sourceMappingURL=rclone.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rclone.js","sourceRoot":"","sources":["../src/rclone.ts"],"names":[],"mappings":"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"}