( root: string )
| 1226 | * open. Returns the moved (assets-relative) and skipped (source) paths. |
| 1227 | */ |
| 1228 | export async function migrateLooseAssets( |
| 1229 | root: string |
| 1230 | ): Promise<{ moved: string[]; skipped: string[] }> { |
| 1231 | const moved: string[] = [] |
| 1232 | const skipped: string[] = [] |
| 1233 | const assetsAbs = path.join(root, ASSETS_DIR) |
| 1234 | |
| 1235 | const moveIntoAssets = async (srcRel: string): Promise<void> => { |
| 1236 | const destRel = `${ASSETS_DIR}/${path.basename(srcRel)}` |
| 1237 | if (toPosix(srcRel) === destRel) return |
| 1238 | const destAbs = resolveSafe(root, destRel) |
| 1239 | try { |
| 1240 | await fs.access(destAbs) // a same-named asset already exists — don't clobber/rename |
| 1241 | skipped.push(toPosix(srcRel)) |
| 1242 | return |
| 1243 | } catch { |
| 1244 | /* destination free */ |
| 1245 | } |
| 1246 | await fs.mkdir(assetsAbs, { recursive: true }) |
| 1247 | await fs.rename(resolveSafe(root, srcRel), destAbs) |
| 1248 | moved.push(destRel) |
| 1249 | } |
| 1250 | |
| 1251 | // 1. Loose asset files sitting directly at the vault root. |
| 1252 | // |
| 1253 | // Skip this for a vault managed by another app — an `.obsidian/` directory |
| 1254 | // marks an Obsidian vault. Those loose root files (`.canvas`, images, PDFs, |
| 1255 | // anything) are the user's own, not ZenNotes' legacy attachments, and |
| 1256 | // silently relocating them into `assets/` surprised users who pointed |
| 1257 | // ZenNotes at an existing Obsidian vault (#202). The legacy ZenNotes |
| 1258 | // attachment dirs below never exist in such a vault, so they stay safe. |
| 1259 | let isExternalVault = false |
| 1260 | try { |
| 1261 | await fs.access(path.join(root, '.obsidian')) |
| 1262 | isExternalVault = true |
| 1263 | } catch { |
| 1264 | /* not an Obsidian vault */ |
| 1265 | } |
| 1266 | if (!isExternalVault) { |
| 1267 | let rootEntries: Dirent[] = [] |
| 1268 | try { |
| 1269 | rootEntries = await fs.readdir(root, { withFileTypes: true }) |
| 1270 | } catch { |
| 1271 | rootEntries = [] |
| 1272 | } |
| 1273 | for (const entry of rootEntries) { |
| 1274 | if (entry.name.startsWith('.') || !entry.isFile()) continue |
| 1275 | if (entry.name.toLowerCase().endsWith('.md')) continue // a note |
| 1276 | if (databaseCsvPathFor(entry.name) || isDatabaseInternalPath(entry.name)) continue // a database |
| 1277 | await moveIntoAssets(entry.name) |
| 1278 | } |
| 1279 | } |
| 1280 | |
| 1281 | // 2. Files in the legacy attachment dirs, then drop the dir if it empties. |
| 1282 | for (const dir of LEGACY_ATTACHMENTS_DIRS) { |
| 1283 | let entries: Dirent[] |
| 1284 | try { |
| 1285 | entries = await fs.readdir(path.join(root, dir), { withFileTypes: true }) |
no test coverage detected