MtnMCG

NDISK_defrag

Dec 20th, 2025 (edited)
67
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 24.58 KB | None | 0 0
  1. -- ndisk_defrag.lua
  2. -- NDisk Defragmentation Tool with RAID10 Support
  3. -- Run this WHILE SERVER IS STOPPED to safely redistribute files across all nodes
  4.  
  5. -- SAFETY: This script uses copy-first, delete-last approach
  6. -- Original data is never deleted until new data is verified
  7.  
  8. local modem = peripheral.find("modem")
  9. if not modem then error("No modem found") end
  10. modem.open(5) -- NDisk channel
  11.  
  12. -- Load NDisk secret for authentication
  13. local NDISK_SECRET = nil
  14. if fs.exists(".ndisk_secret") then
  15. local f = fs.open(".ndisk_secret", "r")
  16. NDISK_SECRET = f.readAll()
  17. f.close()
  18. end
  19.  
  20. -- Configuration
  21. local MAX_FILE_SIZE = 64 * 1024 -- 64KB safety limit
  22. local NODE_TIMEOUT = 3 -- Seconds to wait for node responses
  23. local REQUEST_TIMEOUT = 2 -- Seconds for individual requests
  24.  
  25. -- State
  26. local nodes = {} -- hierarchicalNum -> nodeId
  27. local chunkMap = {} -- filename -> {chunkNum -> {primary, mirror} or nodeId}
  28. local stats = {
  29. filesProcessed = 0,
  30. filesSkipped = 0,
  31. filesUpgraded = 0, -- Files upgraded to RAID10
  32. bytesProcessed = 0,
  33. errors = {}
  34. }
  35.  
  36. -- ============================================
  37. -- RAID10 HELPERS
  38. -- ============================================
  39.  
  40. local function getMirrorIndex(primaryIdx, nodeCount)
  41. if nodeCount < 2 then return nil end
  42. local offset = math.floor(nodeCount / 2)
  43. return ((primaryIdx - 1 + offset) % nodeCount) + 1
  44. end
  45.  
  46. local function getSortedNodes()
  47. local sorted = {}
  48. for idx, id in pairs(nodes) do
  49. table.insert(sorted, {idx = idx, id = id})
  50. end
  51. table.sort(sorted, function(a, b) return a.idx < b.idx end)
  52. return sorted
  53. end
  54.  
  55. -- ============================================
  56. -- UTILITY FUNCTIONS
  57. -- ============================================
  58.  
  59. local function log(msg)
  60. local timestamp = os.date("%H:%M:%S")
  61. print("[" .. timestamp .. "] " .. msg)
  62. end
  63.  
  64. local function logError(msg)
  65. table.insert(stats.errors, msg)
  66. term.setTextColor(colors.red)
  67. log("ERROR: " .. msg)
  68. term.setTextColor(colors.white)
  69. end
  70.  
  71. local function logSuccess(msg)
  72. term.setTextColor(colors.lime)
  73. log(msg)
  74. term.setTextColor(colors.white)
  75. end
  76.  
  77. local function logWarn(msg)
  78. term.setTextColor(colors.yellow)
  79. log("WARN: " .. msg)
  80. term.setTextColor(colors.white)
  81. end
  82.  
  83. -- ============================================
  84. -- NODE COMMUNICATION
  85. -- ============================================
  86.  
  87. local function generateRequestId()
  88. return os.getComputerID() .. "_" .. os.epoch("utc") .. "_" .. math.random(10000)
  89. end
  90.  
  91. local function sendRequest(nodeId, request, fallbackNodeId)
  92. request.targetNode = nodeId
  93. request.secret = NDISK_SECRET -- Include auth secret
  94. modem.transmit(5, 5, request)
  95. local timer = os.startTimer(REQUEST_TIMEOUT)
  96.  
  97. while true do
  98. local event, p1, p2, p3, p4 = os.pullEvent()
  99. if event == "modem_message" and type(p4) == "table" and p4.id == request.id then
  100. return p4.response
  101. elseif event == "timer" and p1 == timer then
  102. -- Primary timeout, try fallback
  103. if fallbackNodeId and fallbackNodeId ~= nodeId then
  104. request.targetNode = fallbackNodeId
  105. modem.transmit(5, 5, request)
  106. local timer2 = os.startTimer(REQUEST_TIMEOUT)
  107.  
  108. while true do
  109. local ev2, pp1, pp2, pp3, pp4 = os.pullEvent()
  110. if ev2 == "modem_message" and type(pp4) == "table" and pp4.id == request.id then
  111. return pp4.response
  112. elseif ev2 == "timer" and pp1 == timer2 then
  113. return nil, "Both nodes timeout"
  114. end
  115. end
  116. end
  117. return nil, "Timeout"
  118. end
  119. end
  120. end
  121.  
  122. -- ============================================
  123. -- NODE DISCOVERY
  124. -- ============================================
  125.  
  126. local function discoverNodes()
  127. log("Discovering storage nodes...")
  128. modem.transmit(5, 5, {type = "discovery", secret = NDISK_SECRET})
  129.  
  130. local discovered = {}
  131. local timer = os.startTimer(NODE_TIMEOUT)
  132.  
  133. while true do
  134. local event, p1, p2, p3, p4 = os.pullEvent()
  135. if event == "modem_message" and type(p4) == "table" and p4.type == "discovery_response" then
  136. discovered[p4.id] = true
  137. elseif event == "timer" and p1 == timer then
  138. break
  139. end
  140. end
  141.  
  142. -- Convert to sorted list for consistent ordering
  143. local nodeList = {}
  144. for id, _ in pairs(discovered) do
  145. table.insert(nodeList, id)
  146. end
  147. table.sort(nodeList)
  148.  
  149. -- Assign hierarchical numbers
  150. nodes = {}
  151. for i, id in ipairs(nodeList) do
  152. nodes[i] = id
  153. end
  154.  
  155. return #nodeList
  156. end
  157.  
  158. local function saveNodeIds()
  159. local file = fs.open("ids.txt", "w")
  160. for hierarchicalNum, nodeId in pairs(nodes) do
  161. file.writeLine(nodeId .. " " .. hierarchicalNum)
  162. end
  163. file.close()
  164. log("Saved " .. #nodes .. " nodes to ids.txt")
  165. end
  166.  
  167. -- ============================================
  168. -- CHUNK MAP (RAID10 FORMAT)
  169. -- ============================================
  170.  
  171. local function loadChunkMap()
  172. chunkMap = {}
  173. if fs.exists("chunklocation.txt") then
  174. local file = fs.open("chunklocation.txt", "r")
  175. for line in file.readLine do
  176. local parts = {}
  177. for part in line:gmatch("%S+") do
  178. table.insert(parts, part)
  179. end
  180.  
  181. if #parts >= 3 then
  182. local filename = parts[1]
  183. local chunkMatch = parts[2]:match("chunk(%d+)")
  184. local chunkNum = chunkMatch and tonumber(chunkMatch) or nil
  185. local primaryId = tonumber(parts[3])
  186. local mirrorId = parts[4] and tonumber(parts[4]) or nil
  187.  
  188. -- Skip malformed lines
  189. if filename and chunkNum and primaryId then
  190. chunkMap[filename] = chunkMap[filename] or {}
  191.  
  192. if mirrorId and mirrorId ~= primaryId then
  193. chunkMap[filename][chunkNum] = {primary = primaryId, mirror = mirrorId}
  194. else
  195. chunkMap[filename][chunkNum] = {primary = primaryId, mirror = nil}
  196. end
  197. end
  198. end
  199. end
  200. file.close()
  201. end
  202. end
  203.  
  204. local function saveChunkMap()
  205. local file = fs.open("chunklocation.txt", "w")
  206. for filename, chunks in pairs(chunkMap) do
  207. for chunkNum, info in pairs(chunks) do
  208. if type(info) == "table" then
  209. local mirrorId = info.mirror or info.primary
  210. file.writeLine(filename .. " chunk" .. chunkNum .. " " .. info.primary .. " " .. mirrorId)
  211. else
  212. file.writeLine(filename .. " chunk" .. chunkNum .. " " .. info)
  213. end
  214. end
  215. end
  216. file.close()
  217. log("Saved chunk map")
  218. end
  219.  
  220. local function backupChunkMap()
  221. if fs.exists("chunklocation.txt") then
  222. if fs.exists("chunklocation.txt.backup") then
  223. fs.delete("chunklocation.txt.backup")
  224. end
  225. fs.copy("chunklocation.txt", "chunklocation.txt.backup")
  226. log("Backed up chunklocation.txt")
  227. end
  228. end
  229.  
  230. -- ============================================
  231. -- FILE OPERATIONS (RAID10)
  232. -- ============================================
  233.  
  234. local function readFileFromNodes(path)
  235. if not chunkMap[path] then
  236. return nil, "File not found in chunk map"
  237. end
  238.  
  239. local contentChunks = {}
  240. local chunkCount = 0
  241. for _ in pairs(chunkMap[path]) do chunkCount = chunkCount + 1 end
  242.  
  243. for chunkNum = 1, chunkCount do
  244. local chunkInfo = chunkMap[path][chunkNum]
  245. if not chunkInfo then
  246. return nil, "Missing chunk " .. chunkNum .. " info"
  247. end
  248.  
  249. local primaryId, mirrorId
  250. if type(chunkInfo) == "table" then
  251. primaryId = chunkInfo.primary
  252. mirrorId = chunkInfo.mirror
  253. else
  254. primaryId = chunkInfo
  255. mirrorId = nil
  256. end
  257.  
  258. local response = sendRequest(primaryId, {
  259. id = generateRequestId(),
  260. action = "read",
  261. path = path,
  262. chunk = chunkNum
  263. }, mirrorId)
  264.  
  265. if response and response.success then
  266. contentChunks[chunkNum] = response.content or ""
  267. else
  268. return nil, "Failed to read chunk " .. chunkNum
  269. end
  270. end
  271.  
  272. -- Ensure no nil values before concat
  273. for i = 1, chunkCount do
  274. if contentChunks[i] == nil then
  275. return nil, "Missing content for chunk " .. i
  276. end
  277. end
  278.  
  279. return table.concat(contentChunks)
  280. end
  281.  
  282. local function writeFileToNodesRaid10(path, content)
  283. local sortedNodes = getSortedNodes()
  284. local nodeCount = #sortedNodes
  285.  
  286. if nodeCount == 0 then
  287. return false, "No nodes available"
  288. end
  289.  
  290. local chunkSize = math.ceil(#content / nodeCount)
  291. if chunkSize == 0 then chunkSize = 1 end
  292.  
  293. local newChunks = {}
  294.  
  295. for i, nodeInfo in ipairs(sortedNodes) do
  296. local start = (i - 1) * chunkSize + 1
  297. if start > #content and i > 1 then break end
  298.  
  299. local chunk = content:sub(start, math.min(start + chunkSize - 1, #content))
  300. if #content == 0 and i == 1 then chunk = "" end
  301.  
  302. local primaryId = nodeInfo.id
  303. local mirrorIdx = getMirrorIndex(i, nodeCount)
  304. local mirrorId = mirrorIdx and sortedNodes[mirrorIdx] and sortedNodes[mirrorIdx].id or nil
  305.  
  306. -- Write to primary
  307. local response = sendRequest(primaryId, {
  308. id = generateRequestId(),
  309. action = "write",
  310. path = path,
  311. chunk = i,
  312. content = chunk
  313. })
  314.  
  315. if not response or not response.success then
  316. return false, "Failed to write chunk " .. i .. " to primary"
  317. end
  318.  
  319. -- Write to mirror
  320. if mirrorId and mirrorId ~= primaryId then
  321. sendRequest(mirrorId, {
  322. id = generateRequestId(),
  323. action = "write",
  324. path = path,
  325. chunk = i,
  326. content = chunk
  327. })
  328. end
  329.  
  330. newChunks[i] = {primary = primaryId, mirror = mirrorId}
  331.  
  332. if start + chunkSize > #content then break end
  333. end
  334.  
  335. return true, newChunks
  336. end
  337.  
  338. local function deleteChunksFromNodes(path, chunks)
  339. for chunkNum, chunkInfo in pairs(chunks) do
  340. local primaryId, mirrorId
  341. if type(chunkInfo) == "table" then
  342. primaryId = chunkInfo.primary
  343. mirrorId = chunkInfo.mirror
  344. else
  345. primaryId = chunkInfo
  346. mirrorId = nil
  347. end
  348.  
  349. sendRequest(primaryId, {
  350. id = generateRequestId(),
  351. action = "delete",
  352. path = path,
  353. chunk = chunkNum
  354. })
  355.  
  356. if mirrorId and mirrorId ~= primaryId then
  357. sendRequest(mirrorId, {
  358. id = generateRequestId(),
  359. action = "delete",
  360. path = path,
  361. chunk = chunkNum
  362. })
  363. end
  364. end
  365. end
  366.  
  367. -- ============================================
  368. -- DEFRAG LOGIC (RAID10 AWARE)
  369. -- ============================================
  370.  
  371. local function isRaid10(chunkInfo)
  372. return type(chunkInfo) == "table" and chunkInfo.mirror and chunkInfo.mirror ~= chunkInfo.primary
  373. end
  374.  
  375. local function defragFile(path)
  376. -- Step 1: Read original content
  377. local content, err = readFileFromNodes(path)
  378. if not content then
  379. return false, "Read failed: " .. (err or "unknown")
  380. end
  381.  
  382. -- Step 2: Check size limit
  383. if #content > MAX_FILE_SIZE then
  384. return false, "File too large (" .. math.floor(#content/1024) .. "KB > 64KB limit)"
  385. end
  386.  
  387. -- Step 3: Check current state
  388. local currentChunkCount = 0
  389. local hasRaid10 = true
  390. for _, chunkInfo in pairs(chunkMap[path]) do
  391. currentChunkCount = currentChunkCount + 1
  392. if not isRaid10(chunkInfo) then
  393. hasRaid10 = false
  394. end
  395. end
  396.  
  397. local nodeCount = #getSortedNodes()
  398.  
  399. -- Already optimal if: correct chunk count AND has RAID10
  400. if currentChunkCount == nodeCount and hasRaid10 then
  401. return true, "Already optimal (RAID10)"
  402. end
  403.  
  404. -- Step 4: Store original chunks for rollback
  405. local originalChunks = {}
  406. for k, v in pairs(chunkMap[path]) do
  407. if type(v) == "table" then
  408. originalChunks[k] = {primary = v.primary, mirror = v.mirror}
  409. else
  410. originalChunks[k] = v
  411. end
  412. end
  413.  
  414. -- Step 5: Write with RAID10
  415. local writeSuccess, newChunks = writeFileToNodesRaid10(path, content)
  416. if not writeSuccess then
  417. chunkMap[path] = originalChunks
  418. return false, "Write failed: " .. (newChunks or "unknown")
  419. end
  420.  
  421. chunkMap[path] = newChunks
  422.  
  423. -- Step 6: Verify
  424. local verifyContent, verifyErr = readFileFromNodes(path)
  425. if verifyContent ~= content then
  426. chunkMap[path] = originalChunks
  427. return false, "Verification failed: " .. (verifyErr or "content mismatch")
  428. end
  429.  
  430. -- Step 7: Delete orphaned chunks
  431. local usedNodes = {}
  432. for _, info in pairs(newChunks) do
  433. if type(info) == "table" then
  434. usedNodes[info.primary] = true
  435. if info.mirror then usedNodes[info.mirror] = true end
  436. else
  437. usedNodes[info] = true
  438. end
  439. end
  440.  
  441. for chunkNum, chunkInfo in pairs(originalChunks) do
  442. local oldPrimary, oldMirror
  443. if type(chunkInfo) == "table" then
  444. oldPrimary = chunkInfo.primary
  445. oldMirror = chunkInfo.mirror
  446. else
  447. oldPrimary = chunkInfo
  448. oldMirror = nil
  449. end
  450.  
  451. if oldPrimary and not usedNodes[oldPrimary] then
  452. sendRequest(oldPrimary, {
  453. id = generateRequestId(),
  454. action = "delete",
  455. path = path,
  456. chunk = chunkNum
  457. })
  458. end
  459. if oldMirror and oldMirror ~= oldPrimary and not usedNodes[oldMirror] then
  460. sendRequest(oldMirror, {
  461. id = generateRequestId(),
  462. action = "delete",
  463. path = path,
  464. chunk = chunkNum
  465. })
  466. end
  467. end
  468.  
  469. stats.filesProcessed = stats.filesProcessed + 1
  470. stats.bytesProcessed = stats.bytesProcessed + #content
  471.  
  472. if not hasRaid10 then
  473. stats.filesUpgraded = stats.filesUpgraded + 1
  474. return true, "Upgraded to RAID10 (" .. nodeCount .. " nodes)"
  475. end
  476.  
  477. return true, "Redistributed to " .. nodeCount .. " nodes (RAID10)"
  478. end
  479.  
  480. local function defragAll()
  481. local files = {}
  482. for path, _ in pairs(chunkMap) do
  483. table.insert(files, path)
  484. end
  485. table.sort(files)
  486.  
  487. log("Starting defrag of " .. #files .. " files...")
  488. print("")
  489.  
  490. for i, path in ipairs(files) do
  491. local progress = math.floor((i / #files) * 100)
  492. term.clearLine()
  493. term.setCursorPos(1, select(2, term.getCursorPos()))
  494. write("[" .. progress .. "%] " .. path)
  495.  
  496. local success, msg = defragFile(path)
  497.  
  498. term.clearLine()
  499. term.setCursorPos(1, select(2, term.getCursorPos()))
  500.  
  501. if success then
  502. if msg == "Already optimal (RAID10)" then
  503. -- Don't spam for already-optimal files
  504. else
  505. logSuccess("OK: " .. path .. " - " .. msg)
  506. end
  507. else
  508. logError(path .. " - " .. msg)
  509. stats.filesSkipped = stats.filesSkipped + 1
  510. end
  511. end
  512.  
  513. print("")
  514. end
  515.  
  516. -- ============================================
  517. -- UI
  518. -- ============================================
  519.  
  520. local function drawHeader()
  521. term.clear()
  522. term.setCursorPos(1, 1)
  523. term.setTextColor(colors.cyan)
  524. print("========================================")
  525. print(" NDisk Defrag Tool (RAID10 Edition) ")
  526. print("========================================")
  527. term.setTextColor(colors.white)
  528. print("")
  529. end
  530.  
  531. local function showStatus()
  532. local fileCount = 0
  533. local totalChunks = 0
  534. local needsDefrag = 0
  535. local needsRaid = 0
  536.  
  537. local sortedNodes = getSortedNodes()
  538. local nodeCount = #sortedNodes
  539.  
  540. for path, chunks in pairs(chunkMap) do
  541. fileCount = fileCount + 1
  542. local chunkCount = 0
  543. local hasRaid = true
  544. for _, info in pairs(chunks) do
  545. chunkCount = chunkCount + 1
  546. totalChunks = totalChunks + 1
  547. if not isRaid10(info) then hasRaid = false end
  548. end
  549. if chunkCount ~= nodeCount then
  550. needsDefrag = needsDefrag + 1
  551. elseif not hasRaid then
  552. needsRaid = needsRaid + 1
  553. end
  554. end
  555.  
  556. print("Current Status:")
  557. print(" Nodes discovered: " .. nodeCount)
  558. print(" Files tracked: " .. fileCount)
  559. print(" Total chunks: " .. totalChunks)
  560.  
  561. if nodeCount >= 2 then
  562. term.setTextColor(colors.lime)
  563. print(" RAID10: Enabled (can lose " .. math.floor(nodeCount/2) .. " nodes)")
  564. term.setTextColor(colors.white)
  565. else
  566. term.setTextColor(colors.yellow)
  567. print(" RAID10: Disabled (need 2+ nodes)")
  568. term.setTextColor(colors.white)
  569. end
  570. print("")
  571.  
  572. if needsDefrag > 0 or needsRaid > 0 then
  573. term.setTextColor(colors.yellow)
  574. if needsDefrag > 0 then
  575. print(" Files needing redistribution: " .. needsDefrag)
  576. end
  577. if needsRaid > 0 then
  578. print(" Files needing RAID10 upgrade: " .. needsRaid)
  579. end
  580. term.setTextColor(colors.white)
  581. else
  582. term.setTextColor(colors.lime)
  583. print(" All files optimal with RAID10!")
  584. term.setTextColor(colors.white)
  585. end
  586. print("")
  587. end
  588.  
  589. local function showNodes()
  590. print("Discovered Nodes:")
  591. local sortedNodes = getSortedNodes()
  592.  
  593. for i, nodeInfo in ipairs(sortedNodes) do
  594. local mirrorIdx = getMirrorIndex(i, #sortedNodes)
  595. local mirrorId = mirrorIdx and sortedNodes[mirrorIdx] and sortedNodes[mirrorIdx].id or "N/A"
  596. print(" [" .. i .. "] ID: " .. nodeInfo.id .. " -> mirrors with [" .. (mirrorIdx or "N/A") .. "]")
  597. end
  598. print("")
  599. end
  600.  
  601. local function showSummary()
  602. print("")
  603. print("========================================")
  604. print(" Defrag Summary")
  605. print("========================================")
  606. print("")
  607. print("Files processed: " .. stats.filesProcessed)
  608. print("Files upgraded: " .. stats.filesUpgraded .. " (to RAID10)")
  609. print("Files skipped: " .. stats.filesSkipped)
  610. print("Bytes processed: " .. math.floor(stats.bytesProcessed / 1024) .. " KB")
  611. print("")
  612.  
  613. if #stats.errors > 0 then
  614. term.setTextColor(colors.red)
  615. print("Errors encountered:")
  616. for i, err in ipairs(stats.errors) do
  617. if i <= 5 then
  618. print(" - " .. err)
  619. end
  620. end
  621. if #stats.errors > 5 then
  622. print(" ... and " .. (#stats.errors - 5) .. " more")
  623. end
  624. term.setTextColor(colors.white)
  625. else
  626. term.setTextColor(colors.lime)
  627. print("No errors!")
  628. term.setTextColor(colors.white)
  629. end
  630. print("")
  631. end
  632.  
  633. local function confirm(msg)
  634. term.setTextColor(colors.yellow)
  635. write(msg .. " [y/N]: ")
  636. term.setTextColor(colors.white)
  637. local input = read()
  638. return input:lower() == "y" or input:lower() == "yes"
  639. end
  640.  
  641. -- ============================================
  642. -- ORPHAN CLEANUP (SCAN ONLY - NO AUTO DELETE)
  643. -- ============================================
  644.  
  645. local function isValidChunk(filename)
  646. return filename:match("_chunk_%d+$") ~= nil
  647. end
  648.  
  649. local function cleanupOrphans()
  650. print("")
  651. log("Scanning nodes for potential orphans...")
  652. term.setTextColor(colors.orange)
  653. print("NOTE: This is SCAN ONLY - nothing will be deleted automatically")
  654. print("To delete orphans, manually run commands on each node")
  655. term.setTextColor(colors.white)
  656. print("")
  657.  
  658. local sortedNodes = getSortedNodes()
  659.  
  660. for _, nodeInfo in ipairs(sortedNodes) do
  661. local nodeId = nodeInfo.id
  662.  
  663. -- Request file list from node
  664. local response = sendRequest(nodeId, {
  665. id = generateRequestId(),
  666. action = "listFiles"
  667. })
  668.  
  669. if response and response.success and response.files then
  670. local orphans = {}
  671. local valid = 0
  672. for _, filename in ipairs(response.files) do
  673. if isValidChunk(filename) then
  674. valid = valid + 1
  675. else
  676. -- Only flag as potential orphan if it's NOT a known directory
  677. -- Directories like 'students' contain chunk files inside!
  678. table.insert(orphans, filename)
  679. end
  680. end
  681.  
  682. if #orphans > 0 then
  683. term.setTextColor(colors.yellow)
  684. print("Node " .. nodeId .. ": " .. valid .. " chunks, " .. #orphans .. " non-chunk items")
  685. term.setTextColor(colors.white)
  686. for _, name in ipairs(orphans) do
  687. print(" [?] " .. name .. " (may contain data - check manually)")
  688. end
  689. else
  690. term.setTextColor(colors.lime)
  691. print("Node " .. nodeId .. ": " .. valid .. " chunks (clean)")
  692. term.setTextColor(colors.white)
  693. end
  694. else
  695. logWarn("Could not scan node " .. nodeId)
  696. end
  697. end
  698.  
  699. print("")
  700. term.setTextColor(colors.cyan)
  701. print("Scan complete. NO deletions performed.")
  702. print("If you need to delete specific files, do it manually on each node.")
  703. term.setTextColor(colors.white)
  704. end
  705.  
  706. -- ============================================
  707. -- MAIN
  708. -- ============================================
  709.  
  710. local function main()
  711. drawHeader()
  712.  
  713. term.setTextColor(colors.orange)
  714. print("WARNING: Run this ONLY when the main server is STOPPED!")
  715. print("This tool will modify ids.txt and chunklocation.txt.")
  716. term.setTextColor(colors.white)
  717. print("")
  718.  
  719. if not confirm("Have you stopped the main server?") then
  720. print("Aborted. Please stop the server first.")
  721. return
  722. end
  723.  
  724. print("")
  725. log("Loading existing chunk map...")
  726. loadChunkMap()
  727.  
  728. local oldFileCount = 0
  729. for _ in pairs(chunkMap) do oldFileCount = oldFileCount + 1 end
  730. log("Found " .. oldFileCount .. " files in chunk map")
  731.  
  732. log("Backing up chunk map...")
  733. backupChunkMap()
  734.  
  735. print("")
  736. local nodeCount = discoverNodes()
  737.  
  738. if nodeCount == 0 then
  739. logError("No storage nodes found! Make sure nodes are running.")
  740. return
  741. end
  742.  
  743. logSuccess("Discovered " .. nodeCount .. " storage nodes")
  744. showNodes()
  745.  
  746. if not confirm("Save this node list and proceed with defrag?") then
  747. print("Aborted.")
  748. return
  749. end
  750.  
  751. log("Saving node list...")
  752. saveNodeIds()
  753.  
  754. print("")
  755. showStatus()
  756.  
  757. if not confirm("Start defragmentation (will upgrade to RAID10)?") then
  758. print("Node list saved. Defrag skipped.")
  759. return
  760. end
  761.  
  762. print("")
  763. term.setTextColor(colors.cyan)
  764. print("Starting RAID10 defragmentation...")
  765. print("This may take a while. Do not interrupt!")
  766. term.setTextColor(colors.white)
  767. print("")
  768.  
  769. defragAll()
  770.  
  771. log("Saving updated chunk map...")
  772. saveChunkMap()
  773.  
  774. showSummary()
  775.  
  776. term.setTextColor(colors.lime)
  777. print("Defragmentation complete!")
  778. print("All files now have RAID10 redundancy.")
  779. term.setTextColor(colors.white)
  780. print("")
  781.  
  782. -- Offer orphan cleanup
  783. if confirm("Scan nodes for orphaned files/directories?") then
  784. cleanupOrphans()
  785. end
  786.  
  787. print("")
  788. term.setTextColor(colors.lime)
  789. print("You can now restart the main server.")
  790. term.setTextColor(colors.white)
  791. end
  792. -- Run
  793. main()
  794.  
Advertisement
Add Comment
Please, Sign In to add comment