nodio-cli 1.0.0 → 1.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/package.json +2 -2
- package/src/node/runtime.js +10 -0
- package/src/node/storage.js +20 -0
- package/src/server/routes.js +91 -21
- package/src/user/commands.js +17 -1
- package/src/user/index.js +10 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nodio-cli",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.1",
|
|
4
4
|
"description": "Nodio distributed storage network",
|
|
5
5
|
"main": "src/server/index.js",
|
|
6
6
|
"type": "commonjs",
|
|
@@ -30,4 +30,4 @@
|
|
|
30
30
|
"mongoose": "^8.18.0",
|
|
31
31
|
"uuid": "^13.0.0"
|
|
32
32
|
}
|
|
33
|
-
}
|
|
33
|
+
}
|
package/src/node/runtime.js
CHANGED
|
@@ -65,6 +65,16 @@ class NodioNodeRuntime {
|
|
|
65
65
|
}
|
|
66
66
|
});
|
|
67
67
|
|
|
68
|
+
app.delete('/shards/:shardId', async (req, res) => {
|
|
69
|
+
try {
|
|
70
|
+
const { shardId } = req.params;
|
|
71
|
+
const result = await this.shardStore.deleteShard(shardId);
|
|
72
|
+
res.json({ ok: true, shard: result });
|
|
73
|
+
} catch (error) {
|
|
74
|
+
res.status(500).json({ error: error.message });
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
|
|
68
78
|
app.get('/health', (_req, res) => {
|
|
69
79
|
res.json({ ok: true, nodeId: this.nodeId });
|
|
70
80
|
});
|
package/src/node/storage.js
CHANGED
|
@@ -64,6 +64,26 @@ class LocalShardStore {
|
|
|
64
64
|
}
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
+
async deleteShard(shardId) {
|
|
68
|
+
const filePath = this.shardPath(shardId);
|
|
69
|
+
|
|
70
|
+
try {
|
|
71
|
+
await fs.unlink(filePath);
|
|
72
|
+
} catch (error) {
|
|
73
|
+
if (error.code !== 'ENOENT') {
|
|
74
|
+
throw error;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const index = await this.readIndex();
|
|
79
|
+
if (index.shards[shardId]) {
|
|
80
|
+
delete index.shards[shardId];
|
|
81
|
+
await this.writeIndex(index);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return { shardId, deleted: true };
|
|
85
|
+
}
|
|
86
|
+
|
|
67
87
|
async listShardIds() {
|
|
68
88
|
const index = await this.readIndex();
|
|
69
89
|
return Object.keys(index.shards);
|
package/src/server/routes.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
const express = require('express');
|
|
2
|
+
const axios = require('axios');
|
|
2
3
|
const { v4: uuidv4 } = require('uuid');
|
|
3
4
|
const {
|
|
4
5
|
NodeModel,
|
|
@@ -17,6 +18,10 @@ function parsePositiveInt(value, fallback) {
|
|
|
17
18
|
return Number.isInteger(n) && n > 0 ? n : fallback;
|
|
18
19
|
}
|
|
19
20
|
|
|
21
|
+
function normalizeUrl(url) {
|
|
22
|
+
return String(url || '').replace(/\/+$/, '');
|
|
23
|
+
}
|
|
24
|
+
|
|
20
25
|
function buildRoutes(config) {
|
|
21
26
|
const router = express.Router();
|
|
22
27
|
|
|
@@ -84,29 +89,34 @@ function buildRoutes(config) {
|
|
|
84
89
|
await node.save();
|
|
85
90
|
|
|
86
91
|
if (Array.isArray(shardIds) && shardIds.length > 0) {
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
.
|
|
90
|
-
|
|
92
|
+
try {
|
|
93
|
+
const normalizedShardIds = [...new Set(shardIds.filter(Boolean))];
|
|
94
|
+
const knownShards = await ShardModel.find({ shardId: { $in: normalizedShardIds } })
|
|
95
|
+
.select('shardId fileId')
|
|
96
|
+
.lean();
|
|
97
|
+
|
|
98
|
+
for (const shard of knownShards) {
|
|
99
|
+
if (!shard.fileId) {
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
91
102
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
103
|
+
await ShardPlacementModel.updateOne(
|
|
104
|
+
{ shardId: shard.shardId, nodeId },
|
|
105
|
+
{
|
|
106
|
+
$set: {
|
|
107
|
+
status: 'available',
|
|
108
|
+
fileId: shard.fileId
|
|
109
|
+
},
|
|
110
|
+
$setOnInsert: {
|
|
111
|
+
shardId: shard.shardId,
|
|
112
|
+
nodeId
|
|
113
|
+
}
|
|
100
114
|
},
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
}
|
|
107
|
-
},
|
|
108
|
-
{ upsert: true }
|
|
109
|
-
);
|
|
115
|
+
{ upsert: true }
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
} catch (syncError) {
|
|
119
|
+
console.warn('[heartbeat-sync]', nodeId, syncError.message);
|
|
110
120
|
}
|
|
111
121
|
}
|
|
112
122
|
|
|
@@ -313,6 +323,66 @@ function buildRoutes(config) {
|
|
|
313
323
|
}
|
|
314
324
|
});
|
|
315
325
|
|
|
326
|
+
router.delete('/files/:fileId', async (req, res, next) => {
|
|
327
|
+
try {
|
|
328
|
+
const { fileId } = req.params;
|
|
329
|
+
const file = await FileModel.findOne({ fileId }).lean();
|
|
330
|
+
if (!file) {
|
|
331
|
+
return res.status(404).json({ error: 'file not found' });
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
const shards = await ShardModel.find({ fileId }).select('shardId').lean();
|
|
335
|
+
const shardIds = shards.map((shard) => shard.shardId);
|
|
336
|
+
|
|
337
|
+
const placements = await ShardPlacementModel.find({ fileId }).lean();
|
|
338
|
+
const nodeIds = [...new Set(placements.map((placement) => placement.nodeId))];
|
|
339
|
+
|
|
340
|
+
const onlineNodes = await NodeModel.find({ nodeId: { $in: nodeIds }, status: 'online' })
|
|
341
|
+
.select('nodeId url')
|
|
342
|
+
.lean();
|
|
343
|
+
const nodeUrlMap = new Map(onlineNodes.map((node) => [node.nodeId, node.url]));
|
|
344
|
+
|
|
345
|
+
const shardDeleteFailures = [];
|
|
346
|
+
for (const placement of placements) {
|
|
347
|
+
const nodeUrl = nodeUrlMap.get(placement.nodeId);
|
|
348
|
+
if (!nodeUrl) {
|
|
349
|
+
continue;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
const deleteUrl = `${normalizeUrl(nodeUrl)}/shards/${placement.shardId}`;
|
|
353
|
+
try {
|
|
354
|
+
await axios.delete(deleteUrl, { timeout: 15000 });
|
|
355
|
+
} catch (error) {
|
|
356
|
+
shardDeleteFailures.push({
|
|
357
|
+
shardId: placement.shardId,
|
|
358
|
+
nodeId: placement.nodeId,
|
|
359
|
+
error: error.response?.data?.error || error.message
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
await ReplicationTaskModel.deleteMany({
|
|
365
|
+
$or: [
|
|
366
|
+
{ fileId },
|
|
367
|
+
{ shardId: { $in: shardIds } }
|
|
368
|
+
]
|
|
369
|
+
});
|
|
370
|
+
await ShardPlacementModel.deleteMany({ fileId });
|
|
371
|
+
await ShardModel.deleteMany({ fileId });
|
|
372
|
+
await FileModel.deleteOne({ fileId });
|
|
373
|
+
|
|
374
|
+
res.json({
|
|
375
|
+
ok: true,
|
|
376
|
+
fileId,
|
|
377
|
+
deletedShards: shardIds.length,
|
|
378
|
+
deletedPlacements: placements.length,
|
|
379
|
+
shardDeleteFailures
|
|
380
|
+
});
|
|
381
|
+
} catch (error) {
|
|
382
|
+
next(error);
|
|
383
|
+
}
|
|
384
|
+
});
|
|
385
|
+
|
|
316
386
|
router.get('/files/:fileId/manifest', async (req, res, next) => {
|
|
317
387
|
try {
|
|
318
388
|
const { fileId } = req.params;
|
package/src/user/commands.js
CHANGED
|
@@ -199,7 +199,23 @@ async function downloadFile(options) {
|
|
|
199
199
|
console.log(`sizeBytes: ${reconstructed.length}`);
|
|
200
200
|
}
|
|
201
201
|
|
|
202
|
+
async function deleteFile(options) {
|
|
203
|
+
const fileId = options.fileId;
|
|
204
|
+
const serverUrl = options.server;
|
|
205
|
+
|
|
206
|
+
const api = createApiClient(serverUrl);
|
|
207
|
+
const response = await api.delete(`/files/${fileId}`);
|
|
208
|
+
const payload = response.data || {};
|
|
209
|
+
|
|
210
|
+
console.log('Delete complete');
|
|
211
|
+
console.log(`fileId: ${payload.fileId || fileId}`);
|
|
212
|
+
console.log(`deletedShards: ${payload.deletedShards || 0}`);
|
|
213
|
+
console.log(`deletedPlacements: ${payload.deletedPlacements || 0}`);
|
|
214
|
+
console.log(`shardDeleteFailures: ${(payload.shardDeleteFailures || []).length}`);
|
|
215
|
+
}
|
|
216
|
+
|
|
202
217
|
module.exports = {
|
|
203
218
|
uploadFile,
|
|
204
|
-
downloadFile
|
|
219
|
+
downloadFile,
|
|
220
|
+
deleteFile
|
|
205
221
|
};
|
package/src/user/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
const { Command } = require('commander');
|
|
3
|
-
const { uploadFile, downloadFile } = require('./commands');
|
|
3
|
+
const { uploadFile, downloadFile, deleteFile } = require('./commands');
|
|
4
4
|
|
|
5
5
|
const program = new Command();
|
|
6
6
|
|
|
@@ -30,6 +30,15 @@ program
|
|
|
30
30
|
await downloadFile(options);
|
|
31
31
|
});
|
|
32
32
|
|
|
33
|
+
program
|
|
34
|
+
.command('delete')
|
|
35
|
+
.description('Delete a file and its shard replicas from the network')
|
|
36
|
+
.requiredOption('--file-id <id>', 'file ID to delete')
|
|
37
|
+
.option('--server <url>', 'central server URL', 'http://127.0.0.1:4000')
|
|
38
|
+
.action(async (options) => {
|
|
39
|
+
await deleteFile(options);
|
|
40
|
+
});
|
|
41
|
+
|
|
33
42
|
program.parseAsync(process.argv).catch((error) => {
|
|
34
43
|
console.error('[nodio]', error.message);
|
|
35
44
|
process.exit(1);
|