UNPKG

ocfl

Version:
266 lines (229 loc) 8.14 kB
const fs = require('fs-extra'); const path = require('path'); const hasha = require('hasha'); const uuidv5 = require('uuid/v5'); const _ = require('lodash'); const DIGEST_ALGORITHM = 'sha512'; class OcflObject { constructor(path) { this.path = path; this.ocflVersion = '1.0'; this.contentVersion = null; // No content yet this.id = null; // Not set yet this.DIGEST_ALGORITHM = DIGEST_ALGORITHM; } async writeInventories(inv, version) { const main_inv = await fs.writeJson(path.join(this.path, 'inventory.json'), inv, { spaces: 2 }); const version_inv = await fs.writeJson(path.join(this.path, version, 'inventory.json'), inv, { spaces: 2 }); const inv_hash = await this.hash_file(path.join(this.path, 'inventory.json')) const digest_file_v1 = await fs.writeFile(path.join(this.path, version, 'inventory.json.' + DIGEST_ALGORITHM), inv_hash + " inventory.json"); const digest_file = await fs.writeFile(path.join(this.path, 'inventory.json.' + DIGEST_ALGORITHM), inv_hash + " inventory.json"); } // addContent is passed an id and a callback - this takes one argument, the // directory into which content is to be written. // This is called for both new and merged versions: the incoming version is // written as v1 of a new object in the deposit directory, and if it's not the // first version it's then merged with the existing most recent version async addContent(id, writeContent) { // Copy files into v1 const version = "v1" // Always a fresh start as we're not touching an existing repo object const versionPath = path.join(this.path, version, "content"); await fs.ensureDir(versionPath); await writeContent(versionPath); // Make an inventory const inv = await this.inventory(id, versionPath); // Put the inventory in the root AND version dir await this.writeInventories(inv, version) this.contentVersion = await this.determineVersion(); this.id = id; } // preserving the old interface here async importDir(id, sourceDir) { await this.addContent(id, async (targetDir) => { await fs.copy(sourceDir, targetDir); }) } async create(path) { // Creates a blank object with a content dir but no content at <path> if (this.path) { throw new Error("This object has already been initialized at: " + this.path) } this.path = path; const stats = await fs.stat(this.path); if (await fs.pathExists(this.path) && stats.isDirectory()) { const readDir = await fs.readdir(this.path); if (readDir.length <= 0) { // empty so initialise an object here const generateNamaste = await this.generateNamaste(this.path, this.ocflVersion); } else { throw new Error('can\'t initialise an object here as there are already files') } } else { //else if it doesnt it dies throw new Error('directory does not exist'); } } async load(path) { // Tries to load an existing object residing at <path> if (this.path) { throw new Error("This object has already been initialized at: " + this.path) } this.path = path; const stats = await fs.stat(this.path); if (await fs.pathExists(this.path) && stats.isDirectory()) { const ocflVersion = await this.isObject(this.path); if (!ocflVersion) { throw new Error('can\'t initialise an object here as there are already files') } } else { throw new Error(path + ' does not exist or is not a directory'); } } getVersionString(i) { // Make a version name as per the SHOULD in the spec v1..vn // TODO have an option for zero padding return "v" + i } async getInventory() { const inventoryPath = path.join(this.path, "inventory.json"); if (await fs.exists(inventoryPath)) { return await JSON.parse(fs.readFileSync(inventoryPath)); } else { return null; } } async determineVersion() { const inv = await this.getInventory(); if (inv) { return inv.head; } else { return null; } // Here's not how to do it: /* var version = 0; const dirContents = await fs.readdir(this.path); for (let f of dirContents.filter(function(d){return d.match(/^v\d+$/)})){ const v = parseInt(f.replace("v","")); if (v > version) { version = v; } } return version;10. */ // Look at each dir that matches v\d+ } async isObject(aPath) { // TODO: Check if this content root with NAMASTE and returns ocfl version // 0=ocfl_object_1.0 // looks at path and see if the content of the file is // TODO: Make this look for a namaste file beginning with 0=ocfl_object_ and extract the version const theFile = path.join(aPath, "0=" + this.nameVersion(this.ocflVersion)); return await fs.pathExists(theFile); } nameVersion(version) { return 'ocfl_object_' + version; } async generateNamaste(aPath, version) { const fileName = '0=' + this.nameVersion(version); const thePath = path.join(aPath, fileName); const writeFile = await fs.writeFile(thePath, this.nameVersion(version)); const contentDir = await fs.mkdir(path.join(aPath, "v1")); } async digest_dir(dir) { var items = {}; const contents = await fs.readdir(dir); items = _.flatten(await Promise.all(contents.map(async (p1) => { const p = path.join(dir, p1); const stats = await fs.stat(p); if (stats.isDirectory()) { return await this.digest_dir(p); } else { const h = await this.hash_file(p); return [p, h]; } }))); return items; } async hash_file(p) { const hash = await hasha.fromFile(p, { algorithm: DIGEST_ALGORITHM }) return hash; } async removeEmptyDirectories(folder) { // Remove empty directories // Adapted (nade async) from: https://gist.github.com/jakub-g/5903dc7e4028133704a4 if (!folder) { folder = this.path; } const stats = await fs.stat(folder); var isDir = await stats.isDirectory(); if (isDir) { var files = await fs.readdir(folder); if (files.length > 0) { for (let f of files) { var fullPath = path.join(folder, f); await this.removeEmptyDirectories(fullPath); } files = await fs.readdir(folder); } if (files.length == 0) { const rm = await fs.rmdir(folder); return; } } } async inventory(id, dir) { const versionId = "v1"; const inv = { 'id': id, 'type': 'https://ocfl.io/1.0/spec/#inventory', 'digestAlgorithm': DIGEST_ALGORITHM, 'head': versionId, 'versions': { } }; inv.versions[versionId] = { "created": new Date().toISOString(), "state": {} } // TODO Message and state keys in version var hashpairs = await this.digest_dir(dir); var versionState = inv.versions[versionId].state; inv['manifest'] = {}; for (let i = 0; i < hashpairs.length; i += 2) { const thisHash = hashpairs[i + 1] const thisPath = path.relative(this.path, hashpairs[i]) const versionPath = path.relative(path.join("v1", "content"), thisPath) if (!inv['manifest'][thisHash]) { //Store only ONE path inv['manifest'][thisHash] = [thisPath] } else { // TODO DELETE THIS FILE FROM THE OBJECT BEFORE WE STORE IT } if (versionState[thisHash]) { // Store all versionState[thisHash].push(versionPath) } else { versionState[thisHash] = [versionPath] } } return inv } async getFilePath(filePath, version) { const inv = await this.getInventory(); if (!version) { version = inv.head; } const state = inv.versions[version].state; for (let hash of Object.keys(state)) { const aPath = state[hash]; const findPath = _.find(aPath, (p) => p === filePath); if (findPath) { return inv.manifest[hash][0]; } } throw new Error('cannot find file'); } } module.exports = OcflObject;