/**
* Storage Locator
*
* Reads/writes a breadcrumb file at ~/.config/hypernote/storage-locator.json
* so the server can find its storage directory regardless of CWD.
*/
import fs from 'bare-fs'
import path from 'bare-path'
import os from 'bare-os'
const LOCATOR_DIR = path.join(os.homedir(), '.config', 'hypernote')
const LOCATOR_FILE = path.join(LOCATOR_DIR, 'storage-locator.json')
export class StorageLocator {
/**
* Read the stored storage path.
* Returns the path only if it exists AND contains a server-identity.json.
* @returns {string|null}
*/
static read () {
try {
const data = JSON.parse(fs.readFileSync(LOCATOR_FILE, 'utf-8'))
if (!data.storagePath) return null
// Verify the path still has a valid identity
const identityFile = path.join(data.storagePath, 'server-identity.json')
fs.statSync(identityFile)
return data.storagePath
} catch {
return null
}
}
/**
* Read the stored storage path WITHOUT validating the identity file exists.
* Used by guards to check if a locator exists before overwriting it.
* @returns {string|null}
*/
static readRaw () {
try {
const data = JSON.parse(fs.readFileSync(LOCATOR_FILE, 'utf-8'))
return data.storagePath || null
} catch {
return null
}
}
/**
* Write the current storage path as a breadcrumb.
* Called after successful startup so next boot can find storage.
*
* GUARD: Refuses to overwrite if existing locator points to a different path
* that still contains a valid server-identity.json.
*
* @param {string} storagePath - Absolute path to storage directory
* @returns {boolean} true if written, false if refused
*/
static async write (storagePath) {
const resolved = path.resolve(storagePath)
const existing = StorageLocator.readRaw()
if (existing && existing !== resolved) {
// Check if the OLD path still has a valid identity
try {
fs.statSync(path.join(existing, 'server-identity.json'))
console.error(`[Storage] REFUSING to update locator: existing identity at ${existing}`)
console.error(`[Storage] New path ${resolved} would orphan the existing identity`)
console.error(`[Storage] Use --storage=${existing} to use the existing identity`)
return false
} catch {
// Old path invalid (identity deleted/moved), safe to overwrite
}
}
try {
await fs.promises.mkdir(LOCATOR_DIR, { recursive: true })
} catch (err) {
if (err.code !== 'EEXIST') throw err
}
const data = {
storagePath: resolved,
updatedAt: Date.now()
}
await fs.promises.writeFile(LOCATOR_FILE, JSON.stringify(data, null, 2) + '\n', {
encoding: 'utf-8',
mode: 0o600
})
return true
}
/** @returns {string} Locator file path (for diagnostics) */
static get filePath () {
return LOCATOR_FILE
}
}
export default StorageLocator