pauls-dat-api
Version:
Library of functions that make working with dat / hyperdrive easier.
237 lines (207 loc) • 7.03 kB
JavaScript
const path = require('path')
const pump = require('pump')
const {maybe, toBeakerError, toValidEncoding} = require('./common')
const {VALID_PATH_REGEX} = require('./const')
const {
InvalidEncodingError,
InvalidPathError,
ArchiveNotWritableError,
EntryAlreadyExistsError,
ParentFolderDoesntExistError
} = require('beaker-error-constants')
const {stat} = require('./lookup')
const {readdir} = require('./read')
const {unlink, rmdir} = require('./delete')
function writeFile (archive, name, data, opts, cb) {
if (typeof opts === 'function') {
cb = opts
opts = {}
}
return maybe(cb, async function () {
if (typeof opts === 'string') {
opts = { encoding: opts }
}
opts = opts || {}
// ensure we have the archive's private key
if (archive.key && !archive.writable) {
throw new ArchiveNotWritableError()
}
// ensure the target path is valid
if (name.slice(-1) === '/') {
throw new InvalidPathError('Files can not have a trailing slash')
}
if (!VALID_PATH_REGEX.test(name)) {
throw new InvalidPathError('Path contains invalid characters')
}
// ensure the target location is writable
var existingEntry
try { existingEntry = await stat(archive, name) } catch (e) {}
if (existingEntry && !existingEntry.isFile()) {
throw new EntryAlreadyExistsError('Cannot overwrite non-files')
}
// copy ctime from the existing entry
if (existingEntry) {
opts.ctime = existingEntry.ctime
}
// ensure that the parent directory exists
var parentName = path.dirname(name)
if (parentName !== '/' && parentName !== '.') {
var parentEntry
try { parentEntry = await stat(archive, parentName) } catch (e) {}
if (!parentEntry || !parentEntry.isDirectory()) {
throw new ParentFolderDoesntExistError()
}
}
// guess the encoding by the data type
if (!opts.encoding) {
opts.encoding = (typeof data === 'string' ? 'utf8' : 'binary')
}
opts.encoding = toValidEncoding(opts.encoding)
// validate the encoding
if (typeof data === 'string' && !opts.encoding) {
throw new InvalidEncodingError()
}
if (typeof data !== 'string' && opts.encoding) {
throw new InvalidEncodingError()
}
// write
return new Promise((resolve, reject) => {
archive.writeFile(name, data, opts, err => {
if (err) reject(toBeakerError(err, 'writeFile'))
else resolve()
})
})
})
}
function mkdir (archive, name, cb) {
return maybe(cb, async function () {
// ensure we have the archive's private key
if (archive.key && !archive.writable) {
throw new ArchiveNotWritableError()
}
// ensure the target path is valid
if (!VALID_PATH_REGEX.test(name)) {
throw new InvalidPathError('Path contains invalid characters')
}
// ensure the target location is writable
var existingEntry
try { existingEntry = await stat(archive, name) } catch (e) {}
if (name === '/' || existingEntry) {
throw new EntryAlreadyExistsError('Cannot overwrite files or folders')
}
// ensure that the parent directory exists
var parentName = path.dirname(name)
if (parentName !== '/' && parentName !== '.') {
var parentEntry
try { parentEntry = await stat(archive, parentName) } catch (e) {}
if (!parentEntry || !parentEntry.isDirectory()) {
throw new ParentFolderDoesntExistError()
}
}
return new Promise((resolve, reject) => {
archive.mkdir(name, err => {
if (err) reject(toBeakerError(err, 'mkdir'))
else resolve()
})
})
})
}
function copy (archive, oldName, newName, cb) {
return maybe(cb, async function () {
// ensure we have the archive's private key
if (archive.key && !archive.writable) {
throw new ArchiveNotWritableError()
}
// ensure the target path is valid
if (!VALID_PATH_REGEX.test(newName)) {
throw new InvalidPathError('Path contains invalid characters')
}
// ensure that the target path is not a child of the source
if (newName === oldName || newName.startsWith(oldName + '/')) {
throw new InvalidPathError('Cannot move or copy a folder to a destination within itself') // that's some existential shit man
}
// ensure that the parent directory exists
var parentName = path.dirname(newName)
if (parentName !== '/' && parentName !== '.') {
var parentEntry
try { parentEntry = await stat(archive, parentName) } catch (e) {}
if (!parentEntry || !parentEntry.isDirectory()) {
throw new ParentFolderDoesntExistError()
}
}
// do copy
await recurseCopy(archive, oldName, newName)
})
}
function rename (archive, oldName, newName, cb) {
return maybe(cb, async function () {
// ensure the target location is writable
var existingEntry
try { existingEntry = await stat(archive, newName) } catch (e) {}
if (newName === '/' || existingEntry) {
throw new EntryAlreadyExistsError('Cannot overwrite files or folders')
}
// copy the files over
await copy(archive, oldName, newName)
// delete the old files
var st = await stat(archive, oldName)
if (st.isDirectory()) {
await rmdir(archive, oldName, {recursive: true})
} else {
await unlink(archive, oldName)
}
})
}
module.exports = {writeFile, mkdir, copy, rename}
// helpers
// =
function safeStat (archive, path) {
return stat(archive, path).catch(_ => undefined)
}
async function recurseCopy (archive, sourcePath, targetPath) {
// fetch stats
var [sourceStat, targetStat] = await Promise.all([
stat(archive, sourcePath),
safeStat(archive, targetPath)
])
if (targetStat) {
if (sourceStat.isFile() && !targetStat.isFile()) {
// never allow this
throw new EntryAlreadyExistsError(`Cannot copy a file onto a folder (${targetPath})`)
}
if (!sourceStat.isFile() && targetStat.isFile()) {
// never allow this
throw new EntryAlreadyExistsError(`Cannot copy a folder onto a file (${targetPath})`)
}
} else {
if (sourceStat.isDirectory()) {
// make directory
await mkdir(archive, targetPath)
}
}
if (sourceStat.isFile()) {
// copy file
return new Promise((resolve, reject) => {
pump(
archive.createReadStream(sourcePath),
archive.createWriteStream(targetPath),
err => {
if (err) reject(toBeakerError(err, 'createReadStream/createWriteStream'))
else resolve()
}
)
})
} else if (sourceStat.isDirectory()) {
// copy children
var children = await readdir(archive, sourcePath)
for (var i = 0; i < children.length; i++) {
await recurseCopy(
archive,
path.join(sourcePath, children[i]),
path.join(targetPath, children[i])
)
}
} else {
throw new Error('Unexpectedly encountered an entry which is neither a file or directory at', path)
}
}