d-drive-cli 1.2.0 → 1.3.2
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 +2 -0
- package/dist/commands/copy.d.ts +1 -0
- package/dist/commands/copy.js +33 -0
- package/dist/commands/upload.js +81 -23
- package/dist/index.js +8 -1
- package/package.json +1 -1
- package/src/commands/copy.ts +32 -0
- package/src/commands/upload.ts +76 -22
- package/src/index.ts +9 -1
package/README.md
CHANGED
|
@@ -40,6 +40,8 @@ Upload a single file:
|
|
|
40
40
|
d-drive upload ./myfile.txt /backups/
|
|
41
41
|
```
|
|
42
42
|
|
|
43
|
+
Note: For very large files the server exposes a streaming upload endpoint (`POST /api/files/upload/stream`) that accepts multipart uploads and streams chunks directly to the storage backend without full buffering. Use the API streaming endpoint (see `docs/API.md`) for multi-GB uploads or when you need more robust handling for long uploads.
|
|
44
|
+
|
|
43
45
|
Upload a directory recursively:
|
|
44
46
|
|
|
45
47
|
```bash
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function copyCommand(remotePath: string): Promise<void>;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.copyCommand = copyCommand;
|
|
7
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
8
|
+
const ora_1 = __importDefault(require("ora"));
|
|
9
|
+
const api_1 = require("../api");
|
|
10
|
+
async function copyCommand(remotePath) {
|
|
11
|
+
const spinner = (0, ora_1.default)('Finding file...').start();
|
|
12
|
+
try {
|
|
13
|
+
const api = (0, api_1.createApiClient)();
|
|
14
|
+
// Find file by path
|
|
15
|
+
const filesResponse = await api.get('/files', { params: { path: remotePath } });
|
|
16
|
+
const files = filesResponse.data;
|
|
17
|
+
if (files.length === 0) {
|
|
18
|
+
spinner.fail(chalk_1.default.red(`File not found: ${remotePath}`));
|
|
19
|
+
process.exit(1);
|
|
20
|
+
}
|
|
21
|
+
const file = files[0];
|
|
22
|
+
spinner.text = 'Creating copy...';
|
|
23
|
+
const resp = await api.post(`/files/${file.id}/copy`);
|
|
24
|
+
spinner.succeed(chalk_1.default.green('Copy created'));
|
|
25
|
+
const created = resp.data;
|
|
26
|
+
console.log(chalk_1.default.gray(`Created: ${created.path || `/${created.name}`}`));
|
|
27
|
+
}
|
|
28
|
+
catch (error) {
|
|
29
|
+
spinner.fail(chalk_1.default.red('Copy failed'));
|
|
30
|
+
console.error(chalk_1.default.red(error.response?.data?.error || error.message));
|
|
31
|
+
process.exit(1);
|
|
32
|
+
}
|
|
33
|
+
}
|
package/dist/commands/upload.js
CHANGED
|
@@ -13,6 +13,7 @@ const form_data_1 = __importDefault(require("form-data"));
|
|
|
13
13
|
const progress_1 = __importDefault(require("progress"));
|
|
14
14
|
const api_1 = require("../api");
|
|
15
15
|
const glob_1 = require("glob");
|
|
16
|
+
const axios_1 = __importDefault(require("axios"));
|
|
16
17
|
async function uploadCommand(source, destination = '/', options) {
|
|
17
18
|
const spinner = (0, ora_1.default)('Preparing upload...').start();
|
|
18
19
|
try {
|
|
@@ -71,48 +72,105 @@ async function uploadSingleFile(api, filePath, destination, showProgress) {
|
|
|
71
72
|
filename: fileName,
|
|
72
73
|
knownLength: fileSize,
|
|
73
74
|
});
|
|
74
|
-
|
|
75
|
+
// Resolve destination directory to a parentId and send parentId (server-authoritative)
|
|
76
|
+
const parentDir = path_1.default.posix.dirname(destination || '/');
|
|
77
|
+
let parentId = null;
|
|
78
|
+
if (parentDir && parentDir !== '/' && parentDir !== '.') {
|
|
79
|
+
parentId = await ensureFolderExists(api, parentDir);
|
|
80
|
+
}
|
|
81
|
+
if (parentId) {
|
|
82
|
+
formData.append('parentId', parentId);
|
|
83
|
+
}
|
|
75
84
|
// Ensure CLI uploads follow frontend behavior and request server-side encryption by default
|
|
76
85
|
formData.append('encrypt', 'true');
|
|
77
86
|
let progressBar = null;
|
|
78
87
|
if (showProgress) {
|
|
88
|
+
// Use a byte-counting progress bar so we can display an explicit percent.
|
|
79
89
|
progressBar = new progress_1.default('[:bar] :percent :etas', {
|
|
80
90
|
complete: '█',
|
|
81
91
|
incomplete: '░',
|
|
82
92
|
width: 40,
|
|
83
|
-
total:
|
|
93
|
+
total: fileSize,
|
|
84
94
|
});
|
|
85
|
-
// Track bytes read from disk and update progress bar
|
|
86
|
-
let uploaded = 0;
|
|
95
|
+
// Track bytes read from disk and update progress bar by bytes.
|
|
87
96
|
fileStream.on('data', (chunk) => {
|
|
88
97
|
const len = typeof chunk === 'string' ? Buffer.byteLength(chunk) : chunk.length;
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
98
|
+
if (progressBar)
|
|
99
|
+
progressBar.tick(len);
|
|
100
|
+
});
|
|
101
|
+
// When local read finishes, indicate we're waiting for the server to finish
|
|
102
|
+
fileStream.on('end', () => {
|
|
103
|
+
if (progressBar && !progressBar.complete) {
|
|
104
|
+
// ensure the bar shows very near completion but leave finalizing to server response
|
|
105
|
+
try {
|
|
106
|
+
progressBar.update(Math.min(1, (progressBar.curr || 0) / (progressBar.total || 1)));
|
|
107
|
+
}
|
|
108
|
+
catch (_) { }
|
|
93
109
|
}
|
|
110
|
+
console.log(chalk_1.default.gray('\nLocal file read complete — waiting for server to finish...'));
|
|
94
111
|
});
|
|
95
112
|
}
|
|
96
|
-
//
|
|
113
|
+
// Use streaming upload endpoint. Do not force Content-Length so the request
|
|
114
|
+
// can stream large files without buffering the whole body in memory.
|
|
97
115
|
const headers = formData.getHeaders();
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
formData.getLength((err, len) => {
|
|
101
|
-
if (err)
|
|
102
|
-
return reject(err);
|
|
103
|
-
resolve(len);
|
|
104
|
-
});
|
|
105
|
-
});
|
|
106
|
-
headers['Content-Length'] = String(length);
|
|
107
|
-
}
|
|
108
|
-
catch (err) {
|
|
109
|
-
// ignore length error
|
|
110
|
-
}
|
|
111
|
-
await api.post('/files/upload', formData, {
|
|
116
|
+
// axios in Node needs the adapter to handle stream form-data; use api (axios instance)
|
|
117
|
+
await api.post('/files/upload/stream', formData, {
|
|
112
118
|
headers,
|
|
113
119
|
maxContentLength: Infinity,
|
|
114
120
|
maxBodyLength: Infinity,
|
|
121
|
+
// Do not set a timeout for potentially long uploads
|
|
122
|
+
timeout: 0,
|
|
123
|
+
// Allow axios to stream the form-data
|
|
124
|
+
transitional: { forcedJSONParsing: false },
|
|
115
125
|
});
|
|
126
|
+
// Upload complete (server has processed). If progress bar exists, ensure it shows 100%.
|
|
127
|
+
if (progressBar && !progressBar.complete) {
|
|
128
|
+
try {
|
|
129
|
+
progressBar.update(progressBar.total || 1);
|
|
130
|
+
}
|
|
131
|
+
catch (_) { }
|
|
132
|
+
}
|
|
133
|
+
console.log(chalk_1.default.green('\nUpload finished'));
|
|
134
|
+
}
|
|
135
|
+
// Ensure the directory at `dirPath` exists. Returns the `id` of the directory or null for root.
|
|
136
|
+
async function ensureFolderExists(api, dirPath) {
|
|
137
|
+
// Normalize and split
|
|
138
|
+
const normalized = path_1.default.posix.normalize(dirPath);
|
|
139
|
+
if (normalized === '/' || normalized === '.' || normalized === '')
|
|
140
|
+
return null;
|
|
141
|
+
const segments = normalized.split('/').filter(Boolean);
|
|
142
|
+
let currentPath = '';
|
|
143
|
+
let parentId = null;
|
|
144
|
+
for (const seg of segments) {
|
|
145
|
+
currentPath = `${currentPath}/${seg}`;
|
|
146
|
+
try {
|
|
147
|
+
const resp = await api.get('/files', { params: { path: currentPath } });
|
|
148
|
+
const items = resp.data;
|
|
149
|
+
const dir = items.find(i => i.type === 'DIRECTORY');
|
|
150
|
+
if (dir) {
|
|
151
|
+
parentId = dir.id;
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
// Not found — create it
|
|
155
|
+
const createResp = await api.post('/files/directory', { name: seg, parentId: parentId || null, path: currentPath });
|
|
156
|
+
parentId = createResp.data.id;
|
|
157
|
+
}
|
|
158
|
+
catch (err) {
|
|
159
|
+
// If a 409 or other error occurs, try to re-query; otherwise rethrow
|
|
160
|
+
if (axios_1.default.isAxiosError(err) && err.response) {
|
|
161
|
+
// Retry by querying again in case of race
|
|
162
|
+
const retry = await api.get('/files', { params: { path: currentPath } });
|
|
163
|
+
const items = retry.data;
|
|
164
|
+
const dir = items.find(i => i.type === 'DIRECTORY');
|
|
165
|
+
if (dir) {
|
|
166
|
+
parentId = dir.id;
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
throw err;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
return parentId;
|
|
116
174
|
}
|
|
117
175
|
function formatFileSize(bytes) {
|
|
118
176
|
if (bytes === 0)
|
package/dist/index.js
CHANGED
|
@@ -11,11 +11,13 @@ const upload_1 = require("./commands/upload");
|
|
|
11
11
|
const download_1 = require("./commands/download");
|
|
12
12
|
const list_1 = require("./commands/list");
|
|
13
13
|
const delete_1 = require("./commands/delete");
|
|
14
|
+
const copy_1 = require("./commands/copy");
|
|
15
|
+
const pkg = require('../package.json');
|
|
14
16
|
const program = new commander_1.Command();
|
|
15
17
|
program
|
|
16
18
|
.name('d-drive')
|
|
17
19
|
.description('D-Drive CLI - Discord-based cloud storage for developers')
|
|
18
|
-
.version('
|
|
20
|
+
.version(pkg.version || '0.0.0');
|
|
19
21
|
// Config command
|
|
20
22
|
program
|
|
21
23
|
.command('config')
|
|
@@ -52,6 +54,11 @@ program
|
|
|
52
54
|
.option('-r, --recursive', 'Delete directory recursively')
|
|
53
55
|
.option('-f, --force', 'Force deletion without confirmation')
|
|
54
56
|
.action(delete_1.deleteCommand);
|
|
57
|
+
// Copy command
|
|
58
|
+
program
|
|
59
|
+
.command('copy <path>')
|
|
60
|
+
.description('Make a copy of a file in-place (auto-numbered)')
|
|
61
|
+
.action(copy_1.copyCommand);
|
|
55
62
|
// Help command
|
|
56
63
|
program.on('--help', () => {
|
|
57
64
|
console.log('');
|
package/package.json
CHANGED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import ora from 'ora';
|
|
3
|
+
import { createApiClient } from '../api';
|
|
4
|
+
|
|
5
|
+
export async function copyCommand(remotePath: string) {
|
|
6
|
+
const spinner = ora('Finding file...').start();
|
|
7
|
+
|
|
8
|
+
try {
|
|
9
|
+
const api = createApiClient();
|
|
10
|
+
|
|
11
|
+
// Find file by path
|
|
12
|
+
const filesResponse = await api.get('/files', { params: { path: remotePath } });
|
|
13
|
+
const files = filesResponse.data;
|
|
14
|
+
if (files.length === 0) {
|
|
15
|
+
spinner.fail(chalk.red(`File not found: ${remotePath}`));
|
|
16
|
+
process.exit(1);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const file = files[0];
|
|
20
|
+
spinner.text = 'Creating copy...';
|
|
21
|
+
|
|
22
|
+
const resp = await api.post(`/files/${file.id}/copy`);
|
|
23
|
+
|
|
24
|
+
spinner.succeed(chalk.green('Copy created'));
|
|
25
|
+
const created = resp.data;
|
|
26
|
+
console.log(chalk.gray(`Created: ${created.path || `/${created.name}`}`));
|
|
27
|
+
} catch (error: any) {
|
|
28
|
+
spinner.fail(chalk.red('Copy failed'));
|
|
29
|
+
console.error(chalk.red(error.response?.data?.error || error.message));
|
|
30
|
+
process.exit(1);
|
|
31
|
+
}
|
|
32
|
+
}
|
package/src/commands/upload.ts
CHANGED
|
@@ -7,6 +7,7 @@ import FormData from 'form-data';
|
|
|
7
7
|
import ProgressBar from 'progress';
|
|
8
8
|
import { createApiClient } from '../api';
|
|
9
9
|
import { glob } from 'glob';
|
|
10
|
+
import axios from 'axios';
|
|
10
11
|
|
|
11
12
|
interface UploadOptions {
|
|
12
13
|
recursive?: boolean;
|
|
@@ -95,51 +96,104 @@ async function uploadSingleFile(
|
|
|
95
96
|
filename: fileName,
|
|
96
97
|
knownLength: fileSize,
|
|
97
98
|
});
|
|
98
|
-
|
|
99
|
+
// Resolve destination directory to a parentId and send parentId (server-authoritative)
|
|
100
|
+
const parentDir = path.posix.dirname(destination || '/');
|
|
101
|
+
let parentId: string | null = null;
|
|
102
|
+
if (parentDir && parentDir !== '/' && parentDir !== '.') {
|
|
103
|
+
parentId = await ensureFolderExists(api, parentDir);
|
|
104
|
+
}
|
|
105
|
+
if (parentId) {
|
|
106
|
+
formData.append('parentId', parentId);
|
|
107
|
+
}
|
|
99
108
|
// Ensure CLI uploads follow frontend behavior and request server-side encryption by default
|
|
100
109
|
formData.append('encrypt', 'true');
|
|
101
110
|
|
|
102
111
|
let progressBar: ProgressBar | null = null;
|
|
103
112
|
|
|
104
113
|
if (showProgress) {
|
|
114
|
+
// Use a byte-counting progress bar so we can display an explicit percent.
|
|
105
115
|
progressBar = new ProgressBar('[:bar] :percent :etas', {
|
|
106
116
|
complete: '█',
|
|
107
117
|
incomplete: '░',
|
|
108
118
|
width: 40,
|
|
109
|
-
total:
|
|
119
|
+
total: fileSize,
|
|
110
120
|
});
|
|
111
121
|
|
|
112
|
-
// Track bytes read from disk and update progress bar
|
|
113
|
-
let uploaded = 0;
|
|
122
|
+
// Track bytes read from disk and update progress bar by bytes.
|
|
114
123
|
fileStream.on('data', (chunk: Buffer | string) => {
|
|
115
124
|
const len = typeof chunk === 'string' ? Buffer.byteLength(chunk) : chunk.length;
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
125
|
+
if (progressBar) progressBar.tick(len);
|
|
126
|
+
});
|
|
127
|
+
// When local read finishes, indicate we're waiting for the server to finish
|
|
128
|
+
fileStream.on('end', () => {
|
|
129
|
+
if (progressBar && !progressBar.complete) {
|
|
130
|
+
// ensure the bar shows very near completion but leave finalizing to server response
|
|
131
|
+
try { progressBar.update(Math.min(1, (progressBar.curr || 0) / (progressBar.total || 1))); } catch (_) {}
|
|
120
132
|
}
|
|
133
|
+
console.log(chalk.gray('\nLocal file read complete — waiting for server to finish...'));
|
|
121
134
|
});
|
|
122
135
|
}
|
|
123
136
|
|
|
124
|
-
//
|
|
137
|
+
// Use streaming upload endpoint. Do not force Content-Length so the request
|
|
138
|
+
// can stream large files without buffering the whole body in memory.
|
|
125
139
|
const headers = formData.getHeaders();
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
formData.getLength((err: any, len: number) => {
|
|
129
|
-
if (err) return reject(err);
|
|
130
|
-
resolve(len);
|
|
131
|
-
});
|
|
132
|
-
});
|
|
133
|
-
headers['Content-Length'] = String(length);
|
|
134
|
-
} catch (err) {
|
|
135
|
-
// ignore length error
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
await api.post('/files/upload', formData, {
|
|
140
|
+
// axios in Node needs the adapter to handle stream form-data; use api (axios instance)
|
|
141
|
+
await api.post('/files/upload/stream', formData, {
|
|
139
142
|
headers,
|
|
140
143
|
maxContentLength: Infinity,
|
|
141
144
|
maxBodyLength: Infinity,
|
|
145
|
+
// Do not set a timeout for potentially long uploads
|
|
146
|
+
timeout: 0,
|
|
147
|
+
// Allow axios to stream the form-data
|
|
148
|
+
transitional: { forcedJSONParsing: false },
|
|
142
149
|
});
|
|
150
|
+
// Upload complete (server has processed). If progress bar exists, ensure it shows 100%.
|
|
151
|
+
if (progressBar && !progressBar.complete) {
|
|
152
|
+
try { progressBar.update(progressBar.total || 1); } catch (_) {}
|
|
153
|
+
}
|
|
154
|
+
console.log(chalk.green('\nUpload finished'));
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Ensure the directory at `dirPath` exists. Returns the `id` of the directory or null for root.
|
|
158
|
+
async function ensureFolderExists(api: any, dirPath: string): Promise<string | null> {
|
|
159
|
+
// Normalize and split
|
|
160
|
+
const normalized = path.posix.normalize(dirPath);
|
|
161
|
+
if (normalized === '/' || normalized === '.' || normalized === '') return null;
|
|
162
|
+
|
|
163
|
+
const segments = normalized.split('/').filter(Boolean);
|
|
164
|
+
let currentPath = '';
|
|
165
|
+
let parentId: string | null = null;
|
|
166
|
+
|
|
167
|
+
for (const seg of segments) {
|
|
168
|
+
currentPath = `${currentPath}/${seg}`;
|
|
169
|
+
try {
|
|
170
|
+
const resp = await api.get('/files', { params: { path: currentPath } });
|
|
171
|
+
const items = resp.data as any[];
|
|
172
|
+
const dir = items.find(i => i.type === 'DIRECTORY');
|
|
173
|
+
if (dir) {
|
|
174
|
+
parentId = dir.id;
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
177
|
+
// Not found — create it
|
|
178
|
+
const createResp = await api.post('/files/directory', { name: seg, parentId: parentId || null, path: currentPath });
|
|
179
|
+
parentId = createResp.data.id;
|
|
180
|
+
} catch (err: any) {
|
|
181
|
+
// If a 409 or other error occurs, try to re-query; otherwise rethrow
|
|
182
|
+
if (axios.isAxiosError(err) && err.response) {
|
|
183
|
+
// Retry by querying again in case of race
|
|
184
|
+
const retry = await api.get('/files', { params: { path: currentPath } });
|
|
185
|
+
const items = retry.data as any[];
|
|
186
|
+
const dir = items.find(i => i.type === 'DIRECTORY');
|
|
187
|
+
if (dir) {
|
|
188
|
+
parentId = dir.id;
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
throw err;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return parentId;
|
|
143
197
|
}
|
|
144
198
|
|
|
145
199
|
function formatFileSize(bytes: number): string {
|
package/src/index.ts
CHANGED
|
@@ -7,13 +7,15 @@ import { uploadCommand } from './commands/upload';
|
|
|
7
7
|
import { downloadCommand } from './commands/download';
|
|
8
8
|
import { listCommand } from './commands/list';
|
|
9
9
|
import { deleteCommand } from './commands/delete';
|
|
10
|
+
import { copyCommand } from './commands/copy';
|
|
11
|
+
const pkg = require('../package.json');
|
|
10
12
|
|
|
11
13
|
const program = new Command();
|
|
12
14
|
|
|
13
15
|
program
|
|
14
16
|
.name('d-drive')
|
|
15
17
|
.description('D-Drive CLI - Discord-based cloud storage for developers')
|
|
16
|
-
.version('
|
|
18
|
+
.version(pkg.version || '0.0.0');
|
|
17
19
|
|
|
18
20
|
// Config command
|
|
19
21
|
program
|
|
@@ -56,6 +58,12 @@ program
|
|
|
56
58
|
.option('-f, --force', 'Force deletion without confirmation')
|
|
57
59
|
.action(deleteCommand);
|
|
58
60
|
|
|
61
|
+
// Copy command
|
|
62
|
+
program
|
|
63
|
+
.command('copy <path>')
|
|
64
|
+
.description('Make a copy of a file in-place (auto-numbered)')
|
|
65
|
+
.action(copyCommand);
|
|
66
|
+
|
|
59
67
|
// Help command
|
|
60
68
|
program.on('--help', () => {
|
|
61
69
|
console.log('');
|