d-drive-cli 1.0.0
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 +159 -0
- package/dist/api.d.ts +2 -0
- package/dist/api.js +23 -0
- package/dist/commands/config.d.ts +7 -0
- package/dist/commands/config.js +32 -0
- package/dist/commands/delete.d.ts +6 -0
- package/dist/commands/delete.js +50 -0
- package/dist/commands/download.d.ts +5 -0
- package/dist/commands/download.js +74 -0
- package/dist/commands/list.d.ts +5 -0
- package/dist/commands/list.js +57 -0
- package/dist/commands/upload.d.ts +6 -0
- package/dist/commands/upload.js +91 -0
- package/dist/config.d.ts +9 -0
- package/dist/config.js +31 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +66 -0
- package/package.json +42 -0
- package/src/api.ts +21 -0
- package/src/commands/config.ts +36 -0
- package/src/commands/delete.ts +58 -0
- package/src/commands/download.ts +92 -0
- package/src/commands/list.ts +64 -0
- package/src/commands/upload.ts +118 -0
- package/src/config.ts +32 -0
- package/src/index.ts +71 -0
- package/tsconfig.json +18 -0
package/README.md
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
# D-Drive CLI
|
|
2
|
+
|
|
3
|
+
Command-line tool for D-Drive cloud storage.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install -g d-drive-cli
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Configuration
|
|
12
|
+
|
|
13
|
+
First, configure your API key:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
d-drive config --key YOUR_API_KEY
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
You can get an API key from the D-Drive web interface at Settings → API Keys.
|
|
20
|
+
|
|
21
|
+
Optional: Set a custom API URL:
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
d-drive config --url https://your-ddrive-instance.com/api
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
View current configuration:
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
d-drive config --list
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Usage
|
|
34
|
+
|
|
35
|
+
### Upload Files
|
|
36
|
+
|
|
37
|
+
Upload a single file:
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
d-drive upload ./myfile.txt /backups/
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Upload a directory recursively:
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
d-drive upload ./myproject /backups/projects/ -r
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
### Download Files
|
|
50
|
+
|
|
51
|
+
Download a file:
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
d-drive download /backups/myfile.txt ./restored.txt
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### List Files
|
|
58
|
+
|
|
59
|
+
List files in root:
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
d-drive list
|
|
63
|
+
# or
|
|
64
|
+
d-drive ls
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
List files in a directory:
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
d-drive list /backups
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Long format with details:
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
d-drive list /backups -l
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### Delete Files
|
|
80
|
+
|
|
81
|
+
Delete a file:
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
d-drive delete /backups/old-file.txt
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Force delete without confirmation:
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
d-drive delete /backups/old-file.txt -f
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
## Examples
|
|
94
|
+
|
|
95
|
+
### Automated Backups
|
|
96
|
+
|
|
97
|
+
Create a backup script:
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
#!/bin/bash
|
|
101
|
+
# backup.sh
|
|
102
|
+
|
|
103
|
+
# Backup database
|
|
104
|
+
pg_dump mydb > /tmp/backup.sql
|
|
105
|
+
d-drive upload /tmp/backup.sql /backups/database/backup-$(date +%Y%m%d).sql
|
|
106
|
+
|
|
107
|
+
# Backup config files
|
|
108
|
+
d-drive upload /etc/myapp /backups/config/ -r
|
|
109
|
+
|
|
110
|
+
# Cleanup
|
|
111
|
+
rm /tmp/backup.sql
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
Add to crontab for daily backups:
|
|
115
|
+
|
|
116
|
+
```bash
|
|
117
|
+
0 2 * * * /path/to/backup.sh
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
### Continuous Integration
|
|
121
|
+
|
|
122
|
+
Upload build artifacts from CI/CD:
|
|
123
|
+
|
|
124
|
+
```yaml
|
|
125
|
+
# .github/workflows/deploy.yml
|
|
126
|
+
- name: Upload build artifacts
|
|
127
|
+
run: |
|
|
128
|
+
npm run build
|
|
129
|
+
d-drive upload ./dist /releases/${{ github.sha }}/ -r
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
## Options
|
|
133
|
+
|
|
134
|
+
### Global Options
|
|
135
|
+
|
|
136
|
+
- `--version` - Show version number
|
|
137
|
+
- `--help` - Show help
|
|
138
|
+
|
|
139
|
+
### Upload Options
|
|
140
|
+
|
|
141
|
+
- `-r, --recursive` - Upload directory recursively
|
|
142
|
+
- `--no-progress` - Disable progress bar
|
|
143
|
+
|
|
144
|
+
### Download Options
|
|
145
|
+
|
|
146
|
+
- `--no-progress` - Disable progress bar
|
|
147
|
+
|
|
148
|
+
### List Options
|
|
149
|
+
|
|
150
|
+
- `-l, --long` - Use long listing format
|
|
151
|
+
|
|
152
|
+
### Delete Options
|
|
153
|
+
|
|
154
|
+
- `-f, --force` - Force deletion without confirmation
|
|
155
|
+
- `-r, --recursive` - Delete directory recursively
|
|
156
|
+
|
|
157
|
+
## License
|
|
158
|
+
|
|
159
|
+
MIT
|
package/dist/api.d.ts
ADDED
package/dist/api.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
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.createApiClient = createApiClient;
|
|
7
|
+
const axios_1 = __importDefault(require("axios"));
|
|
8
|
+
const config_1 = require("./config");
|
|
9
|
+
function createApiClient() {
|
|
10
|
+
const config = (0, config_1.getConfig)();
|
|
11
|
+
if (!config.apiKey) {
|
|
12
|
+
throw new Error('API key not configured. Run: d-drive config --key YOUR_API_KEY');
|
|
13
|
+
}
|
|
14
|
+
const client = axios_1.default.create({
|
|
15
|
+
baseURL: config.apiUrl,
|
|
16
|
+
headers: {
|
|
17
|
+
Authorization: `Bearer ${config.apiKey}`,
|
|
18
|
+
},
|
|
19
|
+
maxContentLength: Infinity,
|
|
20
|
+
maxBodyLength: Infinity,
|
|
21
|
+
});
|
|
22
|
+
return client;
|
|
23
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
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.configCommand = configCommand;
|
|
7
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
8
|
+
const config_1 = require("../config");
|
|
9
|
+
function configCommand(options) {
|
|
10
|
+
if (options.list) {
|
|
11
|
+
const config = (0, config_1.getAllConfig)();
|
|
12
|
+
console.log(chalk_1.default.bold('Current Configuration:'));
|
|
13
|
+
console.log(chalk_1.default.gray('────────────────────────────────'));
|
|
14
|
+
console.log(`API Key: ${config.apiKey ? chalk_1.default.green('✓ Set') : chalk_1.default.red('✗ Not set')}`);
|
|
15
|
+
console.log(`API URL: ${chalk_1.default.cyan(config.apiUrl || 'Not set')}`);
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
if (options.key) {
|
|
19
|
+
(0, config_1.setConfig)('apiKey', options.key);
|
|
20
|
+
console.log(chalk_1.default.green('✓ API key configured successfully'));
|
|
21
|
+
}
|
|
22
|
+
if (options.url) {
|
|
23
|
+
(0, config_1.setConfig)('apiUrl', options.url);
|
|
24
|
+
console.log(chalk_1.default.green('✓ API URL configured successfully'));
|
|
25
|
+
}
|
|
26
|
+
if (!options.key && !options.url) {
|
|
27
|
+
console.log(chalk_1.default.yellow('No configuration options provided.'));
|
|
28
|
+
console.log('Use: d-drive config --key YOUR_API_KEY');
|
|
29
|
+
console.log(' d-drive config --url https://api.example.com');
|
|
30
|
+
console.log(' d-drive config --list');
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
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.deleteCommand = deleteCommand;
|
|
7
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
8
|
+
const ora_1 = __importDefault(require("ora"));
|
|
9
|
+
const inquirer_1 = __importDefault(require("inquirer"));
|
|
10
|
+
const api_1 = require("../api");
|
|
11
|
+
async function deleteCommand(remotePath, options) {
|
|
12
|
+
const spinner = (0, ora_1.default)('Finding file...').start();
|
|
13
|
+
try {
|
|
14
|
+
const api = (0, api_1.createApiClient)();
|
|
15
|
+
// Get file info
|
|
16
|
+
const filesResponse = await api.get('/files', {
|
|
17
|
+
params: { path: remotePath },
|
|
18
|
+
});
|
|
19
|
+
const files = filesResponse.data;
|
|
20
|
+
if (files.length === 0) {
|
|
21
|
+
spinner.fail(chalk_1.default.red(`File not found: ${remotePath}`));
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
const file = files[0];
|
|
25
|
+
spinner.stop();
|
|
26
|
+
// Confirm deletion
|
|
27
|
+
if (!options.force) {
|
|
28
|
+
const answers = await inquirer_1.default.prompt([
|
|
29
|
+
{
|
|
30
|
+
type: 'confirm',
|
|
31
|
+
name: 'confirm',
|
|
32
|
+
message: `Are you sure you want to delete ${chalk_1.default.cyan(file.name)}?`,
|
|
33
|
+
default: false,
|
|
34
|
+
},
|
|
35
|
+
]);
|
|
36
|
+
if (!answers.confirm) {
|
|
37
|
+
console.log(chalk_1.default.yellow('Deletion cancelled'));
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
const deleteSpinner = (0, ora_1.default)('Deleting...').start();
|
|
42
|
+
await api.delete(`/files/${file.id}`);
|
|
43
|
+
deleteSpinner.succeed(chalk_1.default.green('File deleted successfully'));
|
|
44
|
+
}
|
|
45
|
+
catch (error) {
|
|
46
|
+
spinner.fail(chalk_1.default.red('Delete failed'));
|
|
47
|
+
console.error(chalk_1.default.red(error.response?.data?.error || error.message));
|
|
48
|
+
process.exit(1);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
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.downloadCommand = downloadCommand;
|
|
7
|
+
const fs_extra_1 = __importDefault(require("fs-extra"));
|
|
8
|
+
const path_1 = __importDefault(require("path"));
|
|
9
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
10
|
+
const ora_1 = __importDefault(require("ora"));
|
|
11
|
+
const progress_1 = __importDefault(require("progress"));
|
|
12
|
+
const api_1 = require("../api");
|
|
13
|
+
async function downloadCommand(source, destination = './', options) {
|
|
14
|
+
const spinner = (0, ora_1.default)('Finding file...').start();
|
|
15
|
+
try {
|
|
16
|
+
const api = (0, api_1.createApiClient)();
|
|
17
|
+
// First, get file info
|
|
18
|
+
const filesResponse = await api.get('/files', {
|
|
19
|
+
params: { path: source },
|
|
20
|
+
});
|
|
21
|
+
const files = filesResponse.data;
|
|
22
|
+
if (files.length === 0) {
|
|
23
|
+
spinner.fail(chalk_1.default.red(`File not found: ${source}`));
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
const file = files[0];
|
|
27
|
+
if (file.type === 'DIRECTORY') {
|
|
28
|
+
spinner.fail(chalk_1.default.red('Cannot download directories yet'));
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
spinner.text = 'Downloading...';
|
|
32
|
+
const destPath = path_1.default.resolve(destination);
|
|
33
|
+
await fs_extra_1.default.ensureDir(path_1.default.dirname(destPath));
|
|
34
|
+
const response = await api.get(`/files/${file.id}/download`, {
|
|
35
|
+
responseType: 'stream',
|
|
36
|
+
});
|
|
37
|
+
const fileSize = parseInt(response.headers['content-length'] || '0');
|
|
38
|
+
const writer = fs_extra_1.default.createWriteStream(destPath);
|
|
39
|
+
let progressBar = null;
|
|
40
|
+
if (options.progress !== false && fileSize > 0) {
|
|
41
|
+
spinner.stop();
|
|
42
|
+
progressBar = new progress_1.default('[:bar] :percent :etas', {
|
|
43
|
+
complete: '█',
|
|
44
|
+
incomplete: '░',
|
|
45
|
+
width: 40,
|
|
46
|
+
total: fileSize,
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
let downloadedBytes = 0;
|
|
50
|
+
response.data.on('data', (chunk) => {
|
|
51
|
+
downloadedBytes += chunk.length;
|
|
52
|
+
if (progressBar) {
|
|
53
|
+
progressBar.update(downloadedBytes / fileSize);
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
response.data.pipe(writer);
|
|
57
|
+
await new Promise((resolve, reject) => {
|
|
58
|
+
writer.on('finish', () => resolve());
|
|
59
|
+
writer.on('error', reject);
|
|
60
|
+
});
|
|
61
|
+
if (progressBar) {
|
|
62
|
+
console.log(chalk_1.default.green('\n✓ File downloaded successfully'));
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
65
|
+
spinner.succeed(chalk_1.default.green('File downloaded successfully'));
|
|
66
|
+
}
|
|
67
|
+
console.log(chalk_1.default.gray(`Saved to: ${destPath}`));
|
|
68
|
+
}
|
|
69
|
+
catch (error) {
|
|
70
|
+
spinner.fail(chalk_1.default.red('Download failed'));
|
|
71
|
+
console.error(chalk_1.default.red(error.response?.data?.error || error.message));
|
|
72
|
+
process.exit(1);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
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.listCommand = listCommand;
|
|
7
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
8
|
+
const ora_1 = __importDefault(require("ora"));
|
|
9
|
+
const api_1 = require("../api");
|
|
10
|
+
async function listCommand(remotePath = '/', options) {
|
|
11
|
+
const spinner = (0, ora_1.default)('Fetching files...').start();
|
|
12
|
+
try {
|
|
13
|
+
const api = (0, api_1.createApiClient)();
|
|
14
|
+
const response = await api.get('/files', {
|
|
15
|
+
params: { path: remotePath },
|
|
16
|
+
});
|
|
17
|
+
const files = response.data;
|
|
18
|
+
spinner.stop();
|
|
19
|
+
if (files.length === 0) {
|
|
20
|
+
console.log(chalk_1.default.yellow('No files found'));
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
console.log(chalk_1.default.bold(`\nFiles in ${remotePath}:`));
|
|
24
|
+
console.log(chalk_1.default.gray('────────────────────────────────────────────────────'));
|
|
25
|
+
if (options.long) {
|
|
26
|
+
// Long format
|
|
27
|
+
for (const file of files) {
|
|
28
|
+
const icon = file.type === 'DIRECTORY' ? '📁' : '📄';
|
|
29
|
+
const size = file.type === 'FILE' ? formatFileSize(Number(file.size)) : '-';
|
|
30
|
+
const date = new Date(file.updatedAt).toLocaleString();
|
|
31
|
+
console.log(`${icon} ${chalk_1.default.cyan(file.name.padEnd(30))} ${size.padEnd(10)} ${chalk_1.default.gray(date)}`);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
else {
|
|
35
|
+
// Simple format
|
|
36
|
+
for (const file of files) {
|
|
37
|
+
const icon = file.type === 'DIRECTORY' ? '📁' : '📄';
|
|
38
|
+
console.log(`${icon} ${chalk_1.default.cyan(file.name)}`);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
console.log(chalk_1.default.gray('────────────────────────────────────────────────────'));
|
|
42
|
+
console.log(chalk_1.default.gray(`Total: ${files.length} items`));
|
|
43
|
+
}
|
|
44
|
+
catch (error) {
|
|
45
|
+
spinner.fail(chalk_1.default.red('Failed to list files'));
|
|
46
|
+
console.error(chalk_1.default.red(error.response?.data?.error || error.message));
|
|
47
|
+
process.exit(1);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
function formatFileSize(bytes) {
|
|
51
|
+
if (bytes === 0)
|
|
52
|
+
return '0 Bytes';
|
|
53
|
+
const k = 1024;
|
|
54
|
+
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
|
|
55
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
56
|
+
return Math.round(bytes / Math.pow(k, i) * 100) / 100 + ' ' + sizes[i];
|
|
57
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
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.uploadCommand = uploadCommand;
|
|
7
|
+
const fs_extra_1 = __importDefault(require("fs-extra"));
|
|
8
|
+
const path_1 = __importDefault(require("path"));
|
|
9
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
10
|
+
const ora_1 = __importDefault(require("ora"));
|
|
11
|
+
const form_data_1 = __importDefault(require("form-data"));
|
|
12
|
+
const progress_1 = __importDefault(require("progress"));
|
|
13
|
+
const api_1 = require("../api");
|
|
14
|
+
const glob_1 = require("glob");
|
|
15
|
+
async function uploadCommand(source, destination = '/', options) {
|
|
16
|
+
const spinner = (0, ora_1.default)('Preparing upload...').start();
|
|
17
|
+
try {
|
|
18
|
+
const api = (0, api_1.createApiClient)();
|
|
19
|
+
const sourcePath = path_1.default.resolve(source);
|
|
20
|
+
// Check if source exists
|
|
21
|
+
if (!await fs_extra_1.default.pathExists(sourcePath)) {
|
|
22
|
+
spinner.fail(chalk_1.default.red(`Source not found: ${source}`));
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
const stats = await fs_extra_1.default.stat(sourcePath);
|
|
26
|
+
if (stats.isDirectory()) {
|
|
27
|
+
if (!options.recursive) {
|
|
28
|
+
spinner.fail(chalk_1.default.red('Use -r flag to upload directories'));
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
spinner.text = 'Finding files...';
|
|
32
|
+
const files = await (0, glob_1.glob)('**/*', {
|
|
33
|
+
cwd: sourcePath,
|
|
34
|
+
nodir: true,
|
|
35
|
+
});
|
|
36
|
+
spinner.succeed(chalk_1.default.green(`Found ${files.length} files to upload`));
|
|
37
|
+
for (const file of files) {
|
|
38
|
+
await uploadSingleFile(api, path_1.default.join(sourcePath, file), path_1.default.join(destination, file), options.progress !== false);
|
|
39
|
+
}
|
|
40
|
+
console.log(chalk_1.default.green(`\n✓ Uploaded ${files.length} files successfully`));
|
|
41
|
+
}
|
|
42
|
+
else {
|
|
43
|
+
spinner.stop();
|
|
44
|
+
const fileName = path_1.default.basename(sourcePath);
|
|
45
|
+
const destPath = destination.endsWith('/')
|
|
46
|
+
? path_1.default.join(destination, fileName)
|
|
47
|
+
: destination;
|
|
48
|
+
await uploadSingleFile(api, sourcePath, destPath, options.progress !== false);
|
|
49
|
+
console.log(chalk_1.default.green('✓ File uploaded successfully'));
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
catch (error) {
|
|
53
|
+
spinner.fail(chalk_1.default.red('Upload failed'));
|
|
54
|
+
console.error(chalk_1.default.red(error.response?.data?.error || error.message));
|
|
55
|
+
process.exit(1);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
async function uploadSingleFile(api, filePath, destination, showProgress) {
|
|
59
|
+
const fileName = path_1.default.basename(filePath);
|
|
60
|
+
const fileSize = (await fs_extra_1.default.stat(filePath)).size;
|
|
61
|
+
console.log(chalk_1.default.cyan(`\nUploading: ${fileName}`));
|
|
62
|
+
console.log(chalk_1.default.gray(`Size: ${formatFileSize(fileSize)}`));
|
|
63
|
+
const formData = new form_data_1.default();
|
|
64
|
+
formData.append('file', fs_extra_1.default.createReadStream(filePath));
|
|
65
|
+
formData.append('path', destination);
|
|
66
|
+
let progressBar = null;
|
|
67
|
+
if (showProgress) {
|
|
68
|
+
progressBar = new progress_1.default('[:bar] :percent :etas', {
|
|
69
|
+
complete: '█',
|
|
70
|
+
incomplete: '░',
|
|
71
|
+
width: 40,
|
|
72
|
+
total: fileSize,
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
await api.post('/files/upload', formData, {
|
|
76
|
+
headers: formData.getHeaders(),
|
|
77
|
+
onUploadProgress: (progressEvent) => {
|
|
78
|
+
if (progressBar && progressEvent.loaded) {
|
|
79
|
+
progressBar.update(progressEvent.loaded / fileSize);
|
|
80
|
+
}
|
|
81
|
+
},
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
function formatFileSize(bytes) {
|
|
85
|
+
if (bytes === 0)
|
|
86
|
+
return '0 Bytes';
|
|
87
|
+
const k = 1024;
|
|
88
|
+
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
|
|
89
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
90
|
+
return Math.round(bytes / Math.pow(k, i) * 100) / 100 + ' ' + sizes[i];
|
|
91
|
+
}
|
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
interface Config {
|
|
2
|
+
apiKey?: string;
|
|
3
|
+
apiUrl?: string;
|
|
4
|
+
}
|
|
5
|
+
export declare function getConfig(): Config;
|
|
6
|
+
export declare function setConfig(key: keyof Config, value: string): void;
|
|
7
|
+
export declare function deleteConfig(key: keyof Config): void;
|
|
8
|
+
export declare function getAllConfig(): Config;
|
|
9
|
+
export {};
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
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.getConfig = getConfig;
|
|
7
|
+
exports.setConfig = setConfig;
|
|
8
|
+
exports.deleteConfig = deleteConfig;
|
|
9
|
+
exports.getAllConfig = getAllConfig;
|
|
10
|
+
const conf_1 = __importDefault(require("conf"));
|
|
11
|
+
const config = new conf_1.default({
|
|
12
|
+
projectName: 'd-drive',
|
|
13
|
+
defaults: {
|
|
14
|
+
apiUrl: 'http://localhost:5000/api',
|
|
15
|
+
},
|
|
16
|
+
});
|
|
17
|
+
function getConfig() {
|
|
18
|
+
return {
|
|
19
|
+
apiKey: config.get('apiKey'),
|
|
20
|
+
apiUrl: config.get('apiUrl'),
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
function setConfig(key, value) {
|
|
24
|
+
config.set(key, value);
|
|
25
|
+
}
|
|
26
|
+
function deleteConfig(key) {
|
|
27
|
+
config.delete(key);
|
|
28
|
+
}
|
|
29
|
+
function getAllConfig() {
|
|
30
|
+
return config.store;
|
|
31
|
+
}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
4
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
5
|
+
};
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
const commander_1 = require("commander");
|
|
8
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
9
|
+
const config_1 = require("./commands/config");
|
|
10
|
+
const upload_1 = require("./commands/upload");
|
|
11
|
+
const download_1 = require("./commands/download");
|
|
12
|
+
const list_1 = require("./commands/list");
|
|
13
|
+
const delete_1 = require("./commands/delete");
|
|
14
|
+
const program = new commander_1.Command();
|
|
15
|
+
program
|
|
16
|
+
.name('d-drive')
|
|
17
|
+
.description('D-Drive CLI - Discord-based cloud storage for developers')
|
|
18
|
+
.version('1.0.0');
|
|
19
|
+
// Config command
|
|
20
|
+
program
|
|
21
|
+
.command('config')
|
|
22
|
+
.description('Configure D-Drive CLI')
|
|
23
|
+
.option('-k, --key <apiKey>', 'Set API key')
|
|
24
|
+
.option('-u, --url <url>', 'Set API URL')
|
|
25
|
+
.option('-l, --list', 'List current configuration')
|
|
26
|
+
.action(config_1.configCommand);
|
|
27
|
+
// Upload command
|
|
28
|
+
program
|
|
29
|
+
.command('upload <source> [destination]')
|
|
30
|
+
.description('Upload a file or directory to D-Drive')
|
|
31
|
+
.option('-r, --recursive', 'Upload directory recursively')
|
|
32
|
+
.option('--no-progress', 'Disable progress bar')
|
|
33
|
+
.action(upload_1.uploadCommand);
|
|
34
|
+
// Download command
|
|
35
|
+
program
|
|
36
|
+
.command('download <source> [destination]')
|
|
37
|
+
.description('Download a file from D-Drive')
|
|
38
|
+
.option('--no-progress', 'Disable progress bar')
|
|
39
|
+
.action(download_1.downloadCommand);
|
|
40
|
+
// List command
|
|
41
|
+
program
|
|
42
|
+
.command('list [path]')
|
|
43
|
+
.alias('ls')
|
|
44
|
+
.description('List files in D-Drive')
|
|
45
|
+
.option('-l, --long', 'Use long listing format')
|
|
46
|
+
.action(list_1.listCommand);
|
|
47
|
+
// Delete command
|
|
48
|
+
program
|
|
49
|
+
.command('delete <path>')
|
|
50
|
+
.alias('rm')
|
|
51
|
+
.description('Delete a file or directory from D-Drive')
|
|
52
|
+
.option('-r, --recursive', 'Delete directory recursively')
|
|
53
|
+
.option('-f, --force', 'Force deletion without confirmation')
|
|
54
|
+
.action(delete_1.deleteCommand);
|
|
55
|
+
// Help command
|
|
56
|
+
program.on('--help', () => {
|
|
57
|
+
console.log('');
|
|
58
|
+
console.log(chalk_1.default.bold('Examples:'));
|
|
59
|
+
console.log(' $ d-drive config --key YOUR_API_KEY');
|
|
60
|
+
console.log(' $ d-drive upload ./file.txt /backups/');
|
|
61
|
+
console.log(' $ d-drive upload ./myproject /backups/projects/ -r');
|
|
62
|
+
console.log(' $ d-drive download /backups/file.txt ./restored.txt');
|
|
63
|
+
console.log(' $ d-drive list /backups');
|
|
64
|
+
console.log(' $ d-drive delete /backups/old-file.txt');
|
|
65
|
+
});
|
|
66
|
+
program.parse();
|
package/package.json
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "d-drive-cli",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "D-Drive CLI tool for developers",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"d-drive": "./dist/index.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"build": "tsc",
|
|
11
|
+
"dev": "ts-node src/index.ts",
|
|
12
|
+
"prepublishOnly": "npm run build"
|
|
13
|
+
},
|
|
14
|
+
"keywords": [
|
|
15
|
+
"d-drive",
|
|
16
|
+
"discord",
|
|
17
|
+
"backup",
|
|
18
|
+
"cli"
|
|
19
|
+
],
|
|
20
|
+
"author": "jasonzli-DEV",
|
|
21
|
+
"license": "MIT",
|
|
22
|
+
"dependencies": {
|
|
23
|
+
"commander": "^11.1.0",
|
|
24
|
+
"axios": "^1.6.5",
|
|
25
|
+
"chalk": "^4.1.2",
|
|
26
|
+
"ora": "^5.4.1",
|
|
27
|
+
"conf": "^10.2.0",
|
|
28
|
+
"inquirer": "^8.2.6",
|
|
29
|
+
"form-data": "^4.0.0",
|
|
30
|
+
"fs-extra": "^11.2.0",
|
|
31
|
+
"glob": "^10.3.10",
|
|
32
|
+
"progress": "^2.0.3"
|
|
33
|
+
},
|
|
34
|
+
"devDependencies": {
|
|
35
|
+
"@types/node": "^20.10.7",
|
|
36
|
+
"@types/inquirer": "^8.2.10",
|
|
37
|
+
"@types/fs-extra": "^11.0.4",
|
|
38
|
+
"@types/progress": "^2.0.7",
|
|
39
|
+
"typescript": "^5.3.3",
|
|
40
|
+
"ts-node": "^10.9.2"
|
|
41
|
+
}
|
|
42
|
+
}
|
package/src/api.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import axios, { AxiosInstance } from 'axios';
|
|
2
|
+
import { getConfig } from './config';
|
|
3
|
+
|
|
4
|
+
export function createApiClient(): AxiosInstance {
|
|
5
|
+
const config = getConfig();
|
|
6
|
+
|
|
7
|
+
if (!config.apiKey) {
|
|
8
|
+
throw new Error('API key not configured. Run: d-drive config --key YOUR_API_KEY');
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const client = axios.create({
|
|
12
|
+
baseURL: config.apiUrl,
|
|
13
|
+
headers: {
|
|
14
|
+
Authorization: `Bearer ${config.apiKey}`,
|
|
15
|
+
},
|
|
16
|
+
maxContentLength: Infinity,
|
|
17
|
+
maxBodyLength: Infinity,
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
return client;
|
|
21
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { getConfig, setConfig, getAllConfig } from '../config';
|
|
3
|
+
|
|
4
|
+
interface ConfigOptions {
|
|
5
|
+
key?: string;
|
|
6
|
+
url?: string;
|
|
7
|
+
list?: boolean;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function configCommand(options: ConfigOptions) {
|
|
11
|
+
if (options.list) {
|
|
12
|
+
const config = getAllConfig();
|
|
13
|
+
console.log(chalk.bold('Current Configuration:'));
|
|
14
|
+
console.log(chalk.gray('────────────────────────────────'));
|
|
15
|
+
console.log(`API Key: ${config.apiKey ? chalk.green('✓ Set') : chalk.red('✗ Not set')}`);
|
|
16
|
+
console.log(`API URL: ${chalk.cyan(config.apiUrl || 'Not set')}`);
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
if (options.key) {
|
|
21
|
+
setConfig('apiKey', options.key);
|
|
22
|
+
console.log(chalk.green('✓ API key configured successfully'));
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (options.url) {
|
|
26
|
+
setConfig('apiUrl', options.url);
|
|
27
|
+
console.log(chalk.green('✓ API URL configured successfully'));
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (!options.key && !options.url) {
|
|
31
|
+
console.log(chalk.yellow('No configuration options provided.'));
|
|
32
|
+
console.log('Use: d-drive config --key YOUR_API_KEY');
|
|
33
|
+
console.log(' d-drive config --url https://api.example.com');
|
|
34
|
+
console.log(' d-drive config --list');
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import ora from 'ora';
|
|
3
|
+
import inquirer from 'inquirer';
|
|
4
|
+
import { createApiClient } from '../api';
|
|
5
|
+
|
|
6
|
+
interface DeleteOptions {
|
|
7
|
+
recursive?: boolean;
|
|
8
|
+
force?: boolean;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export async function deleteCommand(remotePath: string, options: DeleteOptions) {
|
|
12
|
+
const spinner = ora('Finding file...').start();
|
|
13
|
+
|
|
14
|
+
try {
|
|
15
|
+
const api = createApiClient();
|
|
16
|
+
|
|
17
|
+
// Get file info
|
|
18
|
+
const filesResponse = await api.get('/files', {
|
|
19
|
+
params: { path: remotePath },
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
const files = filesResponse.data;
|
|
23
|
+
if (files.length === 0) {
|
|
24
|
+
spinner.fail(chalk.red(`File not found: ${remotePath}`));
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const file = files[0];
|
|
29
|
+
spinner.stop();
|
|
30
|
+
|
|
31
|
+
// Confirm deletion
|
|
32
|
+
if (!options.force) {
|
|
33
|
+
const answers = await inquirer.prompt([
|
|
34
|
+
{
|
|
35
|
+
type: 'confirm',
|
|
36
|
+
name: 'confirm',
|
|
37
|
+
message: `Are you sure you want to delete ${chalk.cyan(file.name)}?`,
|
|
38
|
+
default: false,
|
|
39
|
+
},
|
|
40
|
+
]);
|
|
41
|
+
|
|
42
|
+
if (!answers.confirm) {
|
|
43
|
+
console.log(chalk.yellow('Deletion cancelled'));
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const deleteSpinner = ora('Deleting...').start();
|
|
49
|
+
|
|
50
|
+
await api.delete(`/files/${file.id}`);
|
|
51
|
+
|
|
52
|
+
deleteSpinner.succeed(chalk.green('File deleted successfully'));
|
|
53
|
+
} catch (error: any) {
|
|
54
|
+
spinner.fail(chalk.red('Delete failed'));
|
|
55
|
+
console.error(chalk.red(error.response?.data?.error || error.message));
|
|
56
|
+
process.exit(1);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import fs from 'fs-extra';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
import ora from 'ora';
|
|
5
|
+
import ProgressBar from 'progress';
|
|
6
|
+
import { createApiClient } from '../api';
|
|
7
|
+
|
|
8
|
+
interface DownloadOptions {
|
|
9
|
+
progress?: boolean;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export async function downloadCommand(
|
|
13
|
+
source: string,
|
|
14
|
+
destination: string = './',
|
|
15
|
+
options: DownloadOptions
|
|
16
|
+
) {
|
|
17
|
+
const spinner = ora('Finding file...').start();
|
|
18
|
+
|
|
19
|
+
try {
|
|
20
|
+
const api = createApiClient();
|
|
21
|
+
|
|
22
|
+
// First, get file info
|
|
23
|
+
const filesResponse = await api.get('/files', {
|
|
24
|
+
params: { path: source },
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
const files = filesResponse.data;
|
|
28
|
+
if (files.length === 0) {
|
|
29
|
+
spinner.fail(chalk.red(`File not found: ${source}`));
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const file = files[0];
|
|
34
|
+
|
|
35
|
+
if (file.type === 'DIRECTORY') {
|
|
36
|
+
spinner.fail(chalk.red('Cannot download directories yet'));
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
spinner.text = 'Downloading...';
|
|
41
|
+
|
|
42
|
+
const destPath = path.resolve(destination);
|
|
43
|
+
await fs.ensureDir(path.dirname(destPath));
|
|
44
|
+
|
|
45
|
+
const response = await api.get(`/files/${file.id}/download`, {
|
|
46
|
+
responseType: 'stream',
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
const fileSize = parseInt(response.headers['content-length'] || '0');
|
|
50
|
+
const writer = fs.createWriteStream(destPath);
|
|
51
|
+
|
|
52
|
+
let progressBar: ProgressBar | null = null;
|
|
53
|
+
|
|
54
|
+
if (options.progress !== false && fileSize > 0) {
|
|
55
|
+
spinner.stop();
|
|
56
|
+
progressBar = new ProgressBar('[:bar] :percent :etas', {
|
|
57
|
+
complete: '█',
|
|
58
|
+
incomplete: '░',
|
|
59
|
+
width: 40,
|
|
60
|
+
total: fileSize,
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
let downloadedBytes = 0;
|
|
65
|
+
|
|
66
|
+
response.data.on('data', (chunk: Buffer) => {
|
|
67
|
+
downloadedBytes += chunk.length;
|
|
68
|
+
if (progressBar) {
|
|
69
|
+
progressBar.update(downloadedBytes / fileSize);
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
response.data.pipe(writer);
|
|
74
|
+
|
|
75
|
+
await new Promise<void>((resolve, reject) => {
|
|
76
|
+
writer.on('finish', () => resolve());
|
|
77
|
+
writer.on('error', reject);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
if (progressBar) {
|
|
81
|
+
console.log(chalk.green('\n✓ File downloaded successfully'));
|
|
82
|
+
} else {
|
|
83
|
+
spinner.succeed(chalk.green('File downloaded successfully'));
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
console.log(chalk.gray(`Saved to: ${destPath}`));
|
|
87
|
+
} catch (error: any) {
|
|
88
|
+
spinner.fail(chalk.red('Download failed'));
|
|
89
|
+
console.error(chalk.red(error.response?.data?.error || error.message));
|
|
90
|
+
process.exit(1);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import ora from 'ora';
|
|
3
|
+
import { createApiClient } from '../api';
|
|
4
|
+
|
|
5
|
+
interface ListOptions {
|
|
6
|
+
long?: boolean;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export async function listCommand(remotePath: string = '/', options: ListOptions) {
|
|
10
|
+
const spinner = ora('Fetching files...').start();
|
|
11
|
+
|
|
12
|
+
try {
|
|
13
|
+
const api = createApiClient();
|
|
14
|
+
|
|
15
|
+
const response = await api.get('/files', {
|
|
16
|
+
params: { path: remotePath },
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
const files = response.data;
|
|
20
|
+
spinner.stop();
|
|
21
|
+
|
|
22
|
+
if (files.length === 0) {
|
|
23
|
+
console.log(chalk.yellow('No files found'));
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
console.log(chalk.bold(`\nFiles in ${remotePath}:`));
|
|
28
|
+
console.log(chalk.gray('────────────────────────────────────────────────────'));
|
|
29
|
+
|
|
30
|
+
if (options.long) {
|
|
31
|
+
// Long format
|
|
32
|
+
for (const file of files) {
|
|
33
|
+
const icon = file.type === 'DIRECTORY' ? '📁' : '📄';
|
|
34
|
+
const size = file.type === 'FILE' ? formatFileSize(Number(file.size)) : '-';
|
|
35
|
+
const date = new Date(file.updatedAt).toLocaleString();
|
|
36
|
+
|
|
37
|
+
console.log(
|
|
38
|
+
`${icon} ${chalk.cyan(file.name.padEnd(30))} ${size.padEnd(10)} ${chalk.gray(date)}`
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
} else {
|
|
42
|
+
// Simple format
|
|
43
|
+
for (const file of files) {
|
|
44
|
+
const icon = file.type === 'DIRECTORY' ? '📁' : '📄';
|
|
45
|
+
console.log(`${icon} ${chalk.cyan(file.name)}`);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
console.log(chalk.gray('────────────────────────────────────────────────────'));
|
|
50
|
+
console.log(chalk.gray(`Total: ${files.length} items`));
|
|
51
|
+
} catch (error: any) {
|
|
52
|
+
spinner.fail(chalk.red('Failed to list files'));
|
|
53
|
+
console.error(chalk.red(error.response?.data?.error || error.message));
|
|
54
|
+
process.exit(1);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function formatFileSize(bytes: number): string {
|
|
59
|
+
if (bytes === 0) return '0 Bytes';
|
|
60
|
+
const k = 1024;
|
|
61
|
+
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
|
|
62
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
63
|
+
return Math.round(bytes / Math.pow(k, i) * 100) / 100 + ' ' + sizes[i];
|
|
64
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import fs from 'fs-extra';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
import ora from 'ora';
|
|
5
|
+
import FormData from 'form-data';
|
|
6
|
+
import ProgressBar from 'progress';
|
|
7
|
+
import { createApiClient } from '../api';
|
|
8
|
+
import { glob } from 'glob';
|
|
9
|
+
|
|
10
|
+
interface UploadOptions {
|
|
11
|
+
recursive?: boolean;
|
|
12
|
+
progress?: boolean;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export async function uploadCommand(
|
|
16
|
+
source: string,
|
|
17
|
+
destination: string = '/',
|
|
18
|
+
options: UploadOptions
|
|
19
|
+
) {
|
|
20
|
+
const spinner = ora('Preparing upload...').start();
|
|
21
|
+
|
|
22
|
+
try {
|
|
23
|
+
const api = createApiClient();
|
|
24
|
+
const sourcePath = path.resolve(source);
|
|
25
|
+
|
|
26
|
+
// Check if source exists
|
|
27
|
+
if (!await fs.pathExists(sourcePath)) {
|
|
28
|
+
spinner.fail(chalk.red(`Source not found: ${source}`));
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const stats = await fs.stat(sourcePath);
|
|
33
|
+
|
|
34
|
+
if (stats.isDirectory()) {
|
|
35
|
+
if (!options.recursive) {
|
|
36
|
+
spinner.fail(chalk.red('Use -r flag to upload directories'));
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
spinner.text = 'Finding files...';
|
|
41
|
+
const files = await glob('**/*', {
|
|
42
|
+
cwd: sourcePath,
|
|
43
|
+
nodir: true,
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
spinner.succeed(chalk.green(`Found ${files.length} files to upload`));
|
|
47
|
+
|
|
48
|
+
for (const file of files) {
|
|
49
|
+
await uploadSingleFile(
|
|
50
|
+
api,
|
|
51
|
+
path.join(sourcePath, file),
|
|
52
|
+
path.join(destination, file),
|
|
53
|
+
options.progress !== false
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
console.log(chalk.green(`\n✓ Uploaded ${files.length} files successfully`));
|
|
58
|
+
} else {
|
|
59
|
+
spinner.stop();
|
|
60
|
+
const fileName = path.basename(sourcePath);
|
|
61
|
+
const destPath = destination.endsWith('/')
|
|
62
|
+
? path.join(destination, fileName)
|
|
63
|
+
: destination;
|
|
64
|
+
|
|
65
|
+
await uploadSingleFile(api, sourcePath, destPath, options.progress !== false);
|
|
66
|
+
console.log(chalk.green('✓ File uploaded successfully'));
|
|
67
|
+
}
|
|
68
|
+
} catch (error: any) {
|
|
69
|
+
spinner.fail(chalk.red('Upload failed'));
|
|
70
|
+
console.error(chalk.red(error.response?.data?.error || error.message));
|
|
71
|
+
process.exit(1);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async function uploadSingleFile(
|
|
76
|
+
api: any,
|
|
77
|
+
filePath: string,
|
|
78
|
+
destination: string,
|
|
79
|
+
showProgress: boolean
|
|
80
|
+
): Promise<void> {
|
|
81
|
+
const fileName = path.basename(filePath);
|
|
82
|
+
const fileSize = (await fs.stat(filePath)).size;
|
|
83
|
+
|
|
84
|
+
console.log(chalk.cyan(`\nUploading: ${fileName}`));
|
|
85
|
+
console.log(chalk.gray(`Size: ${formatFileSize(fileSize)}`));
|
|
86
|
+
|
|
87
|
+
const formData = new FormData();
|
|
88
|
+
formData.append('file', fs.createReadStream(filePath));
|
|
89
|
+
formData.append('path', destination);
|
|
90
|
+
|
|
91
|
+
let progressBar: ProgressBar | null = null;
|
|
92
|
+
|
|
93
|
+
if (showProgress) {
|
|
94
|
+
progressBar = new ProgressBar('[:bar] :percent :etas', {
|
|
95
|
+
complete: '█',
|
|
96
|
+
incomplete: '░',
|
|
97
|
+
width: 40,
|
|
98
|
+
total: fileSize,
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
await api.post('/files/upload', formData, {
|
|
103
|
+
headers: formData.getHeaders(),
|
|
104
|
+
onUploadProgress: (progressEvent: any) => {
|
|
105
|
+
if (progressBar && progressEvent.loaded) {
|
|
106
|
+
progressBar.update(progressEvent.loaded / fileSize);
|
|
107
|
+
}
|
|
108
|
+
},
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function formatFileSize(bytes: number): string {
|
|
113
|
+
if (bytes === 0) return '0 Bytes';
|
|
114
|
+
const k = 1024;
|
|
115
|
+
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
|
|
116
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
117
|
+
return Math.round(bytes / Math.pow(k, i) * 100) / 100 + ' ' + sizes[i];
|
|
118
|
+
}
|
package/src/config.ts
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import Conf from 'conf';
|
|
2
|
+
|
|
3
|
+
interface Config {
|
|
4
|
+
apiKey?: string;
|
|
5
|
+
apiUrl?: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const config = new Conf<Config>({
|
|
9
|
+
projectName: 'd-drive',
|
|
10
|
+
defaults: {
|
|
11
|
+
apiUrl: 'http://localhost:5000/api',
|
|
12
|
+
},
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
export function getConfig(): Config {
|
|
16
|
+
return {
|
|
17
|
+
apiKey: config.get('apiKey'),
|
|
18
|
+
apiUrl: config.get('apiUrl'),
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function setConfig(key: keyof Config, value: string): void {
|
|
23
|
+
config.set(key, value);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function deleteConfig(key: keyof Config): void {
|
|
27
|
+
config.delete(key);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function getAllConfig(): Config {
|
|
31
|
+
return config.store;
|
|
32
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { Command } from 'commander';
|
|
4
|
+
import chalk from 'chalk';
|
|
5
|
+
import { configCommand } from './commands/config';
|
|
6
|
+
import { uploadCommand } from './commands/upload';
|
|
7
|
+
import { downloadCommand } from './commands/download';
|
|
8
|
+
import { listCommand } from './commands/list';
|
|
9
|
+
import { deleteCommand } from './commands/delete';
|
|
10
|
+
|
|
11
|
+
const program = new Command();
|
|
12
|
+
|
|
13
|
+
program
|
|
14
|
+
.name('d-drive')
|
|
15
|
+
.description('D-Drive CLI - Discord-based cloud storage for developers')
|
|
16
|
+
.version('1.0.0');
|
|
17
|
+
|
|
18
|
+
// Config command
|
|
19
|
+
program
|
|
20
|
+
.command('config')
|
|
21
|
+
.description('Configure D-Drive CLI')
|
|
22
|
+
.option('-k, --key <apiKey>', 'Set API key')
|
|
23
|
+
.option('-u, --url <url>', 'Set API URL')
|
|
24
|
+
.option('-l, --list', 'List current configuration')
|
|
25
|
+
.action(configCommand);
|
|
26
|
+
|
|
27
|
+
// Upload command
|
|
28
|
+
program
|
|
29
|
+
.command('upload <source> [destination]')
|
|
30
|
+
.description('Upload a file or directory to D-Drive')
|
|
31
|
+
.option('-r, --recursive', 'Upload directory recursively')
|
|
32
|
+
.option('--no-progress', 'Disable progress bar')
|
|
33
|
+
.action(uploadCommand);
|
|
34
|
+
|
|
35
|
+
// Download command
|
|
36
|
+
program
|
|
37
|
+
.command('download <source> [destination]')
|
|
38
|
+
.description('Download a file from D-Drive')
|
|
39
|
+
.option('--no-progress', 'Disable progress bar')
|
|
40
|
+
.action(downloadCommand);
|
|
41
|
+
|
|
42
|
+
// List command
|
|
43
|
+
program
|
|
44
|
+
.command('list [path]')
|
|
45
|
+
.alias('ls')
|
|
46
|
+
.description('List files in D-Drive')
|
|
47
|
+
.option('-l, --long', 'Use long listing format')
|
|
48
|
+
.action(listCommand);
|
|
49
|
+
|
|
50
|
+
// Delete command
|
|
51
|
+
program
|
|
52
|
+
.command('delete <path>')
|
|
53
|
+
.alias('rm')
|
|
54
|
+
.description('Delete a file or directory from D-Drive')
|
|
55
|
+
.option('-r, --recursive', 'Delete directory recursively')
|
|
56
|
+
.option('-f, --force', 'Force deletion without confirmation')
|
|
57
|
+
.action(deleteCommand);
|
|
58
|
+
|
|
59
|
+
// Help command
|
|
60
|
+
program.on('--help', () => {
|
|
61
|
+
console.log('');
|
|
62
|
+
console.log(chalk.bold('Examples:'));
|
|
63
|
+
console.log(' $ d-drive config --key YOUR_API_KEY');
|
|
64
|
+
console.log(' $ d-drive upload ./file.txt /backups/');
|
|
65
|
+
console.log(' $ d-drive upload ./myproject /backups/projects/ -r');
|
|
66
|
+
console.log(' $ d-drive download /backups/file.txt ./restored.txt');
|
|
67
|
+
console.log(' $ d-drive list /backups');
|
|
68
|
+
console.log(' $ d-drive delete /backups/old-file.txt');
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
program.parse();
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2020",
|
|
4
|
+
"module": "commonjs",
|
|
5
|
+
"lib": ["ES2020"],
|
|
6
|
+
"outDir": "./dist",
|
|
7
|
+
"rootDir": "./src",
|
|
8
|
+
"strict": true,
|
|
9
|
+
"esModuleInterop": true,
|
|
10
|
+
"skipLibCheck": true,
|
|
11
|
+
"forceConsistentCasingInFileNames": true,
|
|
12
|
+
"resolveJsonModule": true,
|
|
13
|
+
"moduleResolution": "node",
|
|
14
|
+
"declaration": true
|
|
15
|
+
},
|
|
16
|
+
"include": ["src/**/*"],
|
|
17
|
+
"exclude": ["node_modules", "dist"]
|
|
18
|
+
}
|