mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2026-11-13 12:23:34 +01:00
Add EPERM copy fallback
This commit is contained in:
1 parent
743d915220
commit
16aa80ed5c
3 files changed
+138
-2
No files matched your search
@@ -70,11 +70,54 @@ function mayCopyFile (srcStat, src, dest, opts) {
|
||||
}
|
||||
|
||||
function copyFile (srcStat, src, dest, opts) {
|
||||
fs.copyFileSync(src, dest)
|
||||
try {
|
||||
fs.copyFileSync(src, dest)
|
||||
} catch (err) {
|
||||
if (err.code !== 'EPERM' || !srcStat.isFile()) throw err
|
||||
copyFileWithReadWrite(src, dest)
|
||||
}
|
||||
if (opts.preserveTimestamps) handleTimestamps(srcStat.mode, src, dest)
|
||||
return setDestMode(dest, srcStat.mode)
|
||||
}
|
||||
|
||||
function copyFileWithReadWrite (src, dest) {
|
||||
const buffer = Buffer.allocUnsafe(1024 * 1024)
|
||||
const sourceFd = fs.openSync(src, 'r')
|
||||
let destFd
|
||||
let copyError
|
||||
try {
|
||||
destFd = fs.openSync(dest, 'w')
|
||||
let bytesRead
|
||||
while ((bytesRead = fs.readSync(sourceFd, buffer, 0, buffer.length, null)) > 0) {
|
||||
let offset = 0
|
||||
while (offset < bytesRead) {
|
||||
const written = fs.writeSync(destFd, buffer, offset, bytesRead - offset)
|
||||
if (written === 0) throw new Error('Failed to write file while copying')
|
||||
offset += written
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
copyError = err
|
||||
} finally {
|
||||
try {
|
||||
if (destFd !== undefined) fs.closeSync(destFd)
|
||||
} catch (err) {
|
||||
copyError ||= err
|
||||
}
|
||||
try {
|
||||
fs.closeSync(sourceFd)
|
||||
} catch (err) {
|
||||
copyError ||= err
|
||||
}
|
||||
}
|
||||
if (copyError) {
|
||||
if (destFd !== undefined) {
|
||||
try { fs.unlinkSync(dest) } catch (_) {}
|
||||
}
|
||||
throw copyError
|
||||
}
|
||||
}
|
||||
|
||||
function handleTimestamps (srcMode, src, dest) {
|
||||
// Make sure the file is writable before setting the timestamp
|
||||
// otherwise open fails with EPERM when invoked with 'r+'
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
const fs = require('graceful-fs')
|
||||
const path = require('path')
|
||||
const { pipeline } = require('stream')
|
||||
const mkdirs = require('../mkdirs').mkdirs
|
||||
const pathExists = require('../path-exists').pathExists
|
||||
const utimesMillis = require('../util/utimes').utimesMillis
|
||||
@@ -98,10 +99,20 @@ function mayCopyFile (srcStat, src, dest, opts, cb) {
|
||||
}
|
||||
|
||||
function copyFile (srcStat, src, dest, opts, cb) {
|
||||
fs.copyFile(src, dest, err => {
|
||||
const finish = err => {
|
||||
if (err) return cb(err)
|
||||
if (opts.preserveTimestamps) return handleTimestampsAndMode(srcStat.mode, src, dest, cb)
|
||||
return setDestMode(dest, srcStat.mode, cb)
|
||||
}
|
||||
|
||||
fs.copyFile(src, dest, err => {
|
||||
if (err?.code !== 'EPERM' || !srcStat.isFile()) return finish(err)
|
||||
|
||||
// Stream the fallback so database copies do not require a full-file buffer.
|
||||
pipeline(fs.createReadStream(src, { highWaterMark: 1024 * 1024 }), fs.createWriteStream(dest, { highWaterMark: 1024 * 1024 }), streamError => {
|
||||
if (streamError) return fs.unlink(dest, () => cb(streamError))
|
||||
finish()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
const { expect } = require('chai')
|
||||
const sinon = require('sinon')
|
||||
const fs = require('graceful-fs')
|
||||
const os = require('os')
|
||||
const path = require('path')
|
||||
const fsExtra = require('../../server/libs/fsExtra')
|
||||
|
||||
describe('fsExtra copy fallback', () => {
|
||||
let dir
|
||||
let source
|
||||
let dest
|
||||
let contents
|
||||
|
||||
beforeEach(() => {
|
||||
dir = fs.mkdtempSync(path.join(os.tmpdir(), 'abs-copy-'))
|
||||
source = path.join(dir, 'source')
|
||||
dest = path.join(dir, 'dest')
|
||||
contents = Buffer.alloc(2 * 1024 * 1024 + 17, 0x61)
|
||||
fs.writeFileSync(source, contents)
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
sinon.restore()
|
||||
fs.rmSync(dir, { recursive: true, force: true })
|
||||
})
|
||||
|
||||
it('copies through streams when asynchronous copyFile is denied', async () => {
|
||||
sinon.stub(fs, 'copyFile').callsFake((src, target, callback) => {
|
||||
callback(Object.assign(new Error('copyfile denied'), { code: 'EPERM' }))
|
||||
})
|
||||
|
||||
await fsExtra.copy(source, dest)
|
||||
|
||||
expect(fs.readFileSync(dest).equals(contents)).to.be.true
|
||||
})
|
||||
|
||||
it('copies in chunks when synchronous copyFile is denied', () => {
|
||||
sinon.stub(fs, 'copyFileSync').throws(Object.assign(new Error('copyfile denied'), { code: 'EPERM' }))
|
||||
|
||||
fsExtra.copySync(source, dest)
|
||||
|
||||
expect(fs.readFileSync(dest).equals(contents)).to.be.true
|
||||
})
|
||||
|
||||
it('moves across devices when asynchronous copyFile is denied', async () => {
|
||||
sinon.stub(fs, 'rename').callsFake((src, target, callback) => {
|
||||
callback(Object.assign(new Error('cross-device link'), { code: 'EXDEV' }))
|
||||
})
|
||||
sinon.stub(fs, 'copyFile').callsFake((src, target, callback) => {
|
||||
callback(Object.assign(new Error('copyfile denied'), { code: 'EPERM' }))
|
||||
})
|
||||
|
||||
await fsExtra.move(source, dest)
|
||||
|
||||
expect(fs.existsSync(source)).to.be.false
|
||||
expect(fs.readFileSync(dest).equals(contents)).to.be.true
|
||||
})
|
||||
|
||||
it('moves across devices when synchronous copyFile is denied', () => {
|
||||
sinon.stub(fs, 'renameSync').throws(Object.assign(new Error('cross-device link'), { code: 'EXDEV' }))
|
||||
sinon.stub(fs, 'copyFileSync').throws(Object.assign(new Error('copyfile denied'), { code: 'EPERM' }))
|
||||
|
||||
fsExtra.moveSync(source, dest)
|
||||
|
||||
expect(fs.existsSync(source)).to.be.false
|
||||
expect(fs.readFileSync(dest).equals(contents)).to.be.true
|
||||
})
|
||||
|
||||
it('propagates unrelated copy errors', async () => {
|
||||
sinon.stub(fs, 'copyFile').callsFake((src, target, callback) => {
|
||||
callback(Object.assign(new Error('file missing'), { code: 'ENOENT' }))
|
||||
})
|
||||
|
||||
try {
|
||||
await fsExtra.copy(source, dest)
|
||||
expect.fail('Expected copy to fail')
|
||||
} catch (error) {
|
||||
expect(error.code).to.equal('ENOENT')
|
||||
}
|
||||
expect(fs.existsSync(dest)).to.be.false
|
||||
})
|
||||
})
|
||||
Reference in new issue
Block a user