OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-note / lib / storage-locator.js

102 lines · javascript ref:
/**
 * 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