| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278 |
- const fs = require('fs')
- const glob = require('glob')
- const uuid = require('node-uuid')
- const path = require('path')
- const Stream = require('stream')
- const { promisify } = require('util')
- const AbstractPersistor = require('./AbstractPersistor')
- const { NotFoundError, ReadError, WriteError } = require('./Errors')
- const PersistorHelper = require('./PersistorHelper')
- const pipeline = promisify(Stream.pipeline)
- const fsUnlink = promisify(fs.unlink)
- const fsOpen = promisify(fs.open)
- const fsStat = promisify(fs.stat)
- const fsGlob = promisify(glob)
- const filterName = (key) => key.replace(/\//g, '_')
- module.exports = class FSPersistor extends AbstractPersistor {
- constructor(settings) {
- super()
- this.settings = settings
- }
- async sendFile(location, target, source) {
- const filteredTarget = filterName(target)
- // actually copy the file (instead of moving it) to maintain consistent behaviour
- // between the different implementations
- try {
- const sourceStream = fs.createReadStream(source)
- const targetStream = fs.createWriteStream(`${location}/${filteredTarget}`)
- await pipeline(sourceStream, targetStream)
- } catch (err) {
- throw PersistorHelper.wrapError(
- err,
- 'failed to copy the specified file',
- { location, target, source },
- WriteError
- )
- }
- }
- async sendStream(location, target, sourceStream, sourceMd5) {
- const fsPath = await this._writeStream(sourceStream)
- if (!sourceMd5) {
- sourceMd5 = await FSPersistor._getFileMd5HashForPath(fsPath)
- }
- try {
- await this.sendFile(location, target, fsPath)
- const destMd5 = await this.getObjectMd5Hash(location, target)
- if (sourceMd5 !== destMd5) {
- await this._deleteFile(`${location}/${filterName(target)}`)
- throw new WriteError({
- message: 'md5 hash mismatch',
- info: { sourceMd5, destMd5, location, target }
- })
- }
- } finally {
- await this._deleteFile(fsPath)
- }
- }
- // opts may be {start: Number, end: Number}
- async getObjectStream(location, name, opts) {
- const filteredName = filterName(name)
- try {
- opts.fd = await fsOpen(`${location}/${filteredName}`, 'r')
- } catch (err) {
- throw PersistorHelper.wrapError(
- err,
- 'failed to open file for streaming',
- { location, filteredName, opts },
- ReadError
- )
- }
- return fs.createReadStream(null, opts)
- }
- async getRedirectUrl() {
- // not implemented
- return null
- }
- async getObjectSize(location, filename) {
- const fullPath = path.join(location, filterName(filename))
- try {
- const stat = await fsStat(fullPath)
- return stat.size
- } catch (err) {
- throw PersistorHelper.wrapError(
- err,
- 'failed to stat file',
- { location, filename },
- ReadError
- )
- }
- }
- async getObjectMd5Hash(location, filename) {
- const fullPath = path.join(location, filterName(filename))
- try {
- return await FSPersistor._getFileMd5HashForPath(fullPath)
- } catch (err) {
- throw new ReadError({
- message: 'unable to get md5 hash from file',
- info: { location, filename }
- }).withCause(err)
- }
- }
- async copyObject(location, fromName, toName) {
- const filteredFromName = filterName(fromName)
- const filteredToName = filterName(toName)
- try {
- const sourceStream = fs.createReadStream(
- `${location}/${filteredFromName}`
- )
- const targetStream = fs.createWriteStream(`${location}/${filteredToName}`)
- await pipeline(sourceStream, targetStream)
- } catch (err) {
- throw PersistorHelper.wrapError(
- err,
- 'failed to copy file',
- { location, filteredFromName, filteredToName },
- WriteError
- )
- }
- }
- async deleteObject(location, name) {
- const filteredName = filterName(name)
- try {
- await fsUnlink(`${location}/${filteredName}`)
- } catch (err) {
- const wrappedError = PersistorHelper.wrapError(
- err,
- 'failed to delete file',
- { location, filteredName },
- WriteError
- )
- if (!(wrappedError instanceof NotFoundError)) {
- // S3 doesn't give us a 404 when a file wasn't there to be deleted, so we
- // should be consistent here as well
- throw wrappedError
- }
- }
- }
- async deleteDirectory(location, name) {
- const filteredName = filterName(name.replace(/\/$/, ''))
- try {
- await Promise.all(
- (await fsGlob(`${location}/${filteredName}_*`)).map((file) =>
- fsUnlink(file)
- )
- )
- } catch (err) {
- throw PersistorHelper.wrapError(
- err,
- 'failed to delete directory',
- { location, filteredName },
- WriteError
- )
- }
- }
- async checkIfObjectExists(location, name) {
- const filteredName = filterName(name)
- try {
- const stat = await fsStat(`${location}/${filteredName}`)
- return !!stat
- } catch (err) {
- if (err.code === 'ENOENT') {
- return false
- }
- throw PersistorHelper.wrapError(
- err,
- 'failed to stat file',
- { location, filteredName },
- ReadError
- )
- }
- }
- // note, does not recurse into subdirectories, as we use a flattened directory structure
- async directorySize(location, name) {
- const filteredName = filterName(name.replace(/\/$/, ''))
- let size = 0
- try {
- const files = await fsGlob(`${location}/${filteredName}_*`)
- for (const file of files) {
- try {
- const stat = await fsStat(file)
- if (stat.isFile()) {
- size += stat.size
- }
- } catch (err) {
- // ignore files that may have just been deleted
- if (err.code !== 'ENOENT') {
- throw err
- }
- }
- }
- } catch (err) {
- throw PersistorHelper.wrapError(
- err,
- 'failed to get directory size',
- { location, name },
- ReadError
- )
- }
- return size
- }
- _getPath(key) {
- if (key == null) {
- key = uuid.v1()
- }
- key = key.replace(/\//g, '-')
- return path.join(this.settings.paths.uploadFolder, key)
- }
- async _writeStream(stream, key) {
- let timer
- if (this.settings.Metrics) {
- timer = new this.settings.Metrics.Timer('writingFile')
- }
- const fsPath = this._getPath(key)
- const writeStream = fs.createWriteStream(fsPath)
- try {
- await pipeline(stream, writeStream)
- if (timer) {
- timer.done()
- }
- return fsPath
- } catch (err) {
- await this._deleteFile(fsPath)
- throw new WriteError({
- message: 'problem writing file locally',
- info: { err, fsPath }
- }).withCause(err)
- }
- }
- async _deleteFile(fsPath) {
- if (!fsPath) {
- return
- }
- try {
- await fsUnlink(fsPath)
- } catch (err) {
- if (err.code !== 'ENOENT') {
- throw new WriteError({
- message: 'failed to delete file',
- info: { fsPath }
- }).withCause(err)
- }
- }
- }
- static async _getFileMd5HashForPath(fullPath) {
- const stream = fs.createReadStream(fullPath)
- return PersistorHelper.calculateStreamMd5(stream)
- }
- }
|