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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nodio-cli",
3
- "version": "1.0.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
+ }
@@ -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
  });
@@ -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);
@@ -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
- const normalizedShardIds = [...new Set(shardIds.filter(Boolean))];
88
- const knownShards = await ShardModel.find({ shardId: { $in: normalizedShardIds } })
89
- .select('shardId fileId')
90
- .lean();
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
- for (const shard of knownShards) {
93
- await ShardPlacementModel.updateOne(
94
- { shardId: shard.shardId, nodeId },
95
- {
96
- $set: {
97
- status: 'available',
98
- fileId: shard.fileId,
99
- updatedAt: new Date()
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
- $setOnInsert: {
102
- shardId: shard.shardId,
103
- nodeId,
104
- fileId: shard.fileId,
105
- createdAt: new Date()
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;
@@ -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);