| … | … | @@ -1033,10 +1033,163 @@ if (command === 'serve' || !command) { |
| 1033 | 1033 | }) |
| 1034 | 1034 | console.log(result.token) |
| 1035 | 1035 | await store.close() |
| 1036 | } else if (command === 'migrate') { |
| 1037 | const subCommand = args[1] // 'verify' or 'cleanup' |
| 1038 | |
| 1039 | let storage = null |
| 1040 | for (let i = 0; i < args.length; i++) { |
| 1041 | if (args[i].startsWith('--storage=')) storage = args[i].split('=')[1] |
| 1042 | else if (args[i] === '--storage' && args[i + 1]) storage = args[i + 1] |
| 1043 | } |
| 1044 | |
| 1045 | const locatorPath = StorageLocator.read() |
| 1046 | storage = storage || locatorPath || HypernoteServer.resolveDefaultStorage() |
| 1047 | |
| 1048 | const store = new Corestore(storage) |
| 1049 | const registryCore = store.get({ name: 'registry' }) |
| 1050 | const registry = new Hyperbee(registryCore, { keyEncoding: 'utf-8', valueEncoding: 'json' }) |
| 1051 | await registry.ready() |
| 1052 | |
| 1053 | const { extractMediaHashes } = await import('./lib/multiwriter/notebook-apply.js') |
| 1054 | |
| 1055 | const mediaStore = new MediaStore(store) |
| 1056 | await mediaStore.ready() |
| 1057 | |
| 1058 | const driveManager = new DriveManager(store, registry) |
| 1059 | const noteManager = new NoteManager(driveManager) |
| 1060 | |
| 1061 | if (subCommand === 'verify') { |
| 1062 | // Check migration marker |
| 1063 | const marker = await registry.get('migration:v3-media-store') |
| 1064 | if (!marker) { |
| 1065 | console.log('No v3 media migration found. Run the server first to trigger migration.') |
| 1066 | await store.close() |
| 1067 | process.exit(1) |
| 1068 | } |
| 1069 | console.log('Migration marker:', JSON.stringify(marker.value, null, 2)) |
| 1070 | |
| 1071 | // Walk all notes, extract media hashes, verify each exists |
| 1072 | let totalHashes = 0 |
| 1073 | let found = 0 |
| 1074 | let missing = 0 |
| 1075 | const missingList = [] |
| 1076 | |
| 1077 | const notebooks = await noteManager.listNotebooks() |
| 1078 | for (const nb of notebooks) { |
| 1079 | const notes = await noteManager.listNotes(nb.slug) |
| 1080 | for (const note of notes) { |
| 1081 | const hashes = extractMediaHashes(note.content || '') |
| 1082 | for (const hash of hashes) { |
| 1083 | totalHashes++ |
| 1084 | const exists = await mediaStore.has(hash) |
| 1085 | if (exists) { |
| 1086 | found++ |
| 1087 | } else { |
| 1088 | // Check legacy mapping |
| 1089 | const legacy = await mediaStore._index.get('legacy:' + hash) |
| 1090 | if (legacy && legacy.value && legacy.value.hash) { |
| 1091 | const legacyExists = await mediaStore.has(legacy.value.hash) |
| 1092 | if (legacyExists) { found++; continue } |
| 1093 | } |
| 1094 | missing++ |
| 1095 | missingList.push({ notebook: nb.slug, note: note.slug || note.id, hash }) |
| 1096 | } |
| 1097 | } |
| 1098 | } |
| 1099 | } |
| 1100 | |
| 1101 | // Also verify MediaStore blobs are readable |
| 1102 | const allBlobs = await mediaStore.listAll() |
| 1103 | let readable = 0 |
| 1104 | let unreadable = 0 |
| 1105 | for (const blob of allBlobs) { |
| 1106 | try { |
| 1107 | const buf = await mediaStore.get(blob.hash) |
| 1108 | if (buf && buf.length > 0) readable++ |
| 1109 | else unreadable++ |
| 1110 | } catch { |
| 1111 | unreadable++ |
| 1112 | } |
| 1113 | } |
| 1114 | |
| 1115 | console.log('\n=== Migration Verification ===') |
| 1116 | console.log('Media references in notes: ' + totalHashes) |
| 1117 | console.log(' Found in MediaStore: ' + found) |
| 1118 | console.log(' Missing: ' + missing) |
| 1119 | console.log('Blobs in MediaStore: ' + allBlobs.length) |
| 1120 | console.log(' Readable: ' + readable) |
| 1121 | console.log(' Unreadable: ' + unreadable) |
| 1122 | |
| 1123 | if (missing > 0) { |
| 1124 | console.log('\nMissing media:') |
| 1125 | for (const m of missingList) { |
| 1126 | console.log(' ' + m.notebook + '/' + m.note + ' → ' + m.hash) |
| 1127 | } |
| 1128 | console.log('\nVerification FAILED. Do NOT clean up yet.') |
| 1129 | } else { |
| 1130 | // Set verified marker |
| 1131 | await registry.put('migration:v3-verified', { verifiedAt: Date.now(), totalHashes, blobs: allBlobs.length }) |
| 1132 | console.log('\nVerification PASSED. Safe to run: bare hypernote-server.js migrate cleanup') |
| 1133 | } |
| 1134 | |
| 1135 | await store.close() |
| 1136 | |
| 1137 | } else if (subCommand === 'cleanup') { |
| 1138 | // Check verified marker |
| 1139 | const verified = await registry.get('migration:v3-verified') |
| 1140 | if (!verified) { |
| 1141 | console.log('Migration not verified. Run "migrate verify" first.') |
| 1142 | await store.close() |
| 1143 | process.exit(1) |
| 1144 | } |
| 1145 | |
| 1146 | // Delete backup directory |
| 1147 | const backupPath = storage + '.pre-v3-backup' |
| 1148 | try { |
| 1149 | await fs.promises.access(backupPath) |
| 1150 | console.log('Removing backup at ' + backupPath + '...') |
| 1151 | await fs.promises.rm(backupPath, { recursive: true, force: true }) |
| 1152 | console.log('Backup removed.') |
| 1153 | } catch { |
| 1154 | console.log('No backup found at ' + backupPath) |
| 1155 | } |
| 1156 | |
| 1157 | // Clear old Hyperdrive /media/ files |
| 1158 | let cleared = 0 |
| 1159 | const drives = await driveManager.listDrives() |
| 1160 | for (const entry of drives) { |
| 1161 | if (!entry.isLocal) continue |
| 1162 | const drive = await driveManager.getDrive(entry.slug) |
| 1163 | if (!drive) continue |
| 1164 | try { |
| 1165 | const files = [] |
| 1166 | for await (const f of drive.readdir('/media/')) files.push(f) |
| 1167 | for (const f of files) { |
| 1168 | try { |
| 1169 | await drive.clear('/media/' + f) |
| 1170 | cleared++ |
| 1171 | } catch {} |
| 1172 | } |
| 1173 | } catch {} |
| 1174 | } |
| 1175 | |
| 1176 | console.log('Cleared ' + cleared + ' old Hyperdrive media files.') |
| 1177 | console.log('Cleanup complete.') |
| 1178 | |
| 1179 | await store.close() |
| 1180 | |
| 1181 | } else { |
| 1182 | console.log('Usage:') |
| 1183 | console.log(' bare hypernote-server.js migrate verify [--storage=path]') |
| 1184 | console.log(' bare hypernote-server.js migrate cleanup [--storage=path]') |
| 1185 | process.exit(1) |
| 1186 | } |
| 1036 | 1187 | } else { |
| 1037 | 1188 | console.log('Usage:') |
| 1038 | 1189 | console.log(' bare hypernote-server.js [serve] [port] [--storage=path] [--network]') |
| 1039 | 1190 | console.log(' bare hypernote-server.js promote <publicKey|username> [--storage=path]') |
| 1040 | 1191 | console.log(' bare hypernote-server.js new-token [--storage=path]') |
| 1192 | console.log(' bare hypernote-server.js migrate verify [--storage=path]') |
| 1193 | console.log(' bare hypernote-server.js migrate cleanup [--storage=path]') |
| 1041 | 1194 | process.exit(1) |
| 1042 | 1195 | } |