mirror of
https://github.com/overleaf/overleaf.git
synced 2024-11-21 20:47:08 -05:00
Add support to GCS persistor for unlocking files and copying on delete
This commit is contained in:
parent
28c3fe4a56
commit
183cb0179a
7 changed files with 193 additions and 117 deletions
|
@ -9,18 +9,21 @@ const PersistorHelper = require('./PersistorHelper')
|
||||||
|
|
||||||
const pipeline = promisify(Stream.pipeline)
|
const pipeline = promisify(Stream.pipeline)
|
||||||
|
|
||||||
// both of these settings will be null by default except for tests
|
// endpoint settings will be null by default except for tests
|
||||||
// that's OK - GCS uses the locally-configured service account by default
|
// that's OK - GCS uses the locally-configured service account by default
|
||||||
const storage = new Storage(settings.filestore.gcs)
|
const storage = new Storage(settings.filestore.gcs.endpoint)
|
||||||
// workaround for broken uploads with custom endpoints:
|
// workaround for broken uploads with custom endpoints:
|
||||||
// https://github.com/googleapis/nodejs-storage/issues/898
|
// https://github.com/googleapis/nodejs-storage/issues/898
|
||||||
if (settings.filestore.gcs && settings.filestore.gcs.apiEndpoint) {
|
if (
|
||||||
|
settings.filestore.gcs.endpoint &&
|
||||||
|
settings.filestore.gcs.endpoint.apiEndpoint
|
||||||
|
) {
|
||||||
storage.interceptors.push({
|
storage.interceptors.push({
|
||||||
request: function(reqOpts) {
|
request: function(reqOpts) {
|
||||||
const url = new URL(reqOpts.uri)
|
const url = new URL(reqOpts.uri)
|
||||||
url.host = settings.filestore.gcs.apiEndpoint
|
url.host = settings.filestore.gcs.endpoint.apiEndpoint
|
||||||
if (settings.filestore.gcs.apiScheme) {
|
if (settings.filestore.gcs.endpoint.apiScheme) {
|
||||||
url.protocol = settings.filestore.gcs.apiScheme
|
url.protocol = settings.filestore.gcs.endpoint.apiScheme
|
||||||
}
|
}
|
||||||
reqOpts.uri = url.toString()
|
reqOpts.uri = url.toString()
|
||||||
return reqOpts
|
return reqOpts
|
||||||
|
@ -173,10 +176,19 @@ async function getFileMd5Hash(bucketName, key) {
|
||||||
|
|
||||||
async function deleteFile(bucketName, key) {
|
async function deleteFile(bucketName, key) {
|
||||||
try {
|
try {
|
||||||
await storage
|
const file = storage.bucket(bucketName).file(key)
|
||||||
.bucket(bucketName)
|
|
||||||
.file(key)
|
if (settings.filestore.gcs.unlockBeforeDelete) {
|
||||||
.delete()
|
await file.setMetadata({ eventBasedHold: false })
|
||||||
|
}
|
||||||
|
if (settings.filestore.gcs.deletedBucketSuffix) {
|
||||||
|
await file.copy(
|
||||||
|
storage.bucket(
|
||||||
|
`${bucketName}${settings.filestore.gcs.deletedBucketSuffix}`
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
await file.delete()
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const error = PersistorHelper.wrapError(
|
const error = PersistorHelper.wrapError(
|
||||||
err,
|
err,
|
||||||
|
@ -199,9 +211,13 @@ async function deleteDirectory(bucketName, key) {
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await storage
|
const [files] = await storage
|
||||||
.bucket(bucketName)
|
.bucket(bucketName)
|
||||||
.deleteFiles({ directory: key, force: true })
|
.getFiles({ directory: key })
|
||||||
|
|
||||||
|
for (const file of files) {
|
||||||
|
await deleteFile(bucketName, file.name)
|
||||||
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const error = PersistorHelper.wrapError(
|
const error = PersistorHelper.wrapError(
|
||||||
err,
|
err,
|
||||||
|
|
|
@ -93,7 +93,8 @@ function wrapError(error, message, params, ErrorType) {
|
||||||
error instanceof NotFoundError ||
|
error instanceof NotFoundError ||
|
||||||
['NoSuchKey', 'NotFound', 404, 'AccessDenied', 'ENOENT'].includes(
|
['NoSuchKey', 'NotFound', 404, 'AccessDenied', 'ENOENT'].includes(
|
||||||
error.code
|
error.code
|
||||||
)
|
) ||
|
||||||
|
(error.response && error.response.statusCode === 404)
|
||||||
) {
|
) {
|
||||||
return new NotFoundError({
|
return new NotFoundError({
|
||||||
message: 'no such file',
|
message: 'no such file',
|
||||||
|
|
|
@ -35,12 +35,15 @@ settings =
|
||||||
backend: process.env['BACKEND']
|
backend: process.env['BACKEND']
|
||||||
|
|
||||||
gcs:
|
gcs:
|
||||||
if process.env['GCS_API_ENDPOINT']
|
endpoint:
|
||||||
apiEndpoint: process.env['GCS_API_ENDPOINT']
|
if process.env['GCS_API_ENDPOINT']
|
||||||
apiScheme: process.env['GCS_API_SCHEME']
|
apiEndpoint: process.env['GCS_API_ENDPOINT']
|
||||||
projectId: process.env['GCS_PROJECT_ID']
|
apiScheme: process.env['GCS_API_SCHEME']
|
||||||
# only keys that match this regex can be deleted
|
projectId: process.env['GCS_PROJECT_ID']
|
||||||
directoryKeyRegex: new RegExp(process.env['GCS_DIRECTORY_KEY_REGEX'] || "^[0-9a-fA-F]{24}/[0-9a-fA-F]{24}")
|
# only keys that match this regex can be deleted
|
||||||
|
directoryKeyRegex: new RegExp(process.env['GCS_DIRECTORY_KEY_REGEX'] || "^[0-9a-fA-F]{24}/[0-9a-fA-F]{24}")
|
||||||
|
unlockBeforeDelete: process.env['GCS_UNLOCK_BEFORE_DELETE'] == "true" # unlock an event-based hold before deleting. default false
|
||||||
|
deletedBucketSuffix: process.env['GCS_DELETED_BUCKET_SUFFIX'] # if present, copy file to another bucket on delete. default null
|
||||||
|
|
||||||
s3:
|
s3:
|
||||||
if process.env['AWS_ACCESS_KEY_ID']? or process.env['S3_BUCKET_CREDENTIALS']?
|
if process.env['AWS_ACCESS_KEY_ID']? or process.env['S3_BUCKET_CREDENTIALS']?
|
||||||
|
|
|
@ -4,6 +4,7 @@ const fs = require('fs')
|
||||||
const Settings = require('settings-sharelatex')
|
const Settings = require('settings-sharelatex')
|
||||||
const Path = require('path')
|
const Path = require('path')
|
||||||
const FilestoreApp = require('./FilestoreApp')
|
const FilestoreApp = require('./FilestoreApp')
|
||||||
|
const TestHelper = require('./TestHelper')
|
||||||
const rp = require('request-promise-native').defaults({
|
const rp = require('request-promise-native').defaults({
|
||||||
resolveWithFullResponse: true
|
resolveWithFullResponse: true
|
||||||
})
|
})
|
||||||
|
@ -20,28 +21,10 @@ const fsWriteFile = promisify(fs.writeFile)
|
||||||
const fsStat = promisify(fs.stat)
|
const fsStat = promisify(fs.stat)
|
||||||
const pipeline = promisify(Stream.pipeline)
|
const pipeline = promisify(Stream.pipeline)
|
||||||
|
|
||||||
async function getMetric(filestoreUrl, metric) {
|
|
||||||
const res = await rp.get(`${filestoreUrl}/metrics`)
|
|
||||||
expect(res.statusCode).to.equal(200)
|
|
||||||
const metricRegex = new RegExp(`^${metric}{[^}]+} ([0-9]+)$`, 'm')
|
|
||||||
const found = metricRegex.exec(res.body)
|
|
||||||
return parseInt(found ? found[1] : 0) || 0
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!process.env.AWS_ACCESS_KEY_ID) {
|
if (!process.env.AWS_ACCESS_KEY_ID) {
|
||||||
throw new Error('please provide credentials for the AWS S3 test server')
|
throw new Error('please provide credentials for the AWS S3 test server')
|
||||||
}
|
}
|
||||||
|
|
||||||
function streamToString(stream) {
|
|
||||||
const chunks = []
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
stream.on('data', chunk => chunks.push(chunk))
|
|
||||||
stream.on('error', reject)
|
|
||||||
stream.on('end', () => resolve(Buffer.concat(chunks).toString('utf8')))
|
|
||||||
stream.resume()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// store settings for multiple backends, so that we can test each one.
|
// store settings for multiple backends, so that we can test each one.
|
||||||
// fs will always be available - add others if they are configured
|
// fs will always be available - add others if they are configured
|
||||||
const BackendSettings = require('./TestConfig')
|
const BackendSettings = require('./TestConfig')
|
||||||
|
@ -64,10 +47,19 @@ describe('Filestore', function() {
|
||||||
|
|
||||||
if (BackendSettings[backend].gcs) {
|
if (BackendSettings[backend].gcs) {
|
||||||
before(async function() {
|
before(async function() {
|
||||||
const storage = new Storage(Settings.filestore.gcs)
|
const storage = new Storage(Settings.filestore.gcs.endpoint)
|
||||||
await storage.createBucket(process.env.GCS_USER_FILES_BUCKET_NAME)
|
await storage.createBucket(process.env.GCS_USER_FILES_BUCKET_NAME)
|
||||||
await storage.createBucket(process.env.GCS_PUBLIC_FILES_BUCKET_NAME)
|
await storage.createBucket(process.env.GCS_PUBLIC_FILES_BUCKET_NAME)
|
||||||
await storage.createBucket(process.env.GCS_TEMPLATE_FILES_BUCKET_NAME)
|
await storage.createBucket(process.env.GCS_TEMPLATE_FILES_BUCKET_NAME)
|
||||||
|
await storage.createBucket(
|
||||||
|
`${process.env.GCS_USER_FILES_BUCKET_NAME}-deleted`
|
||||||
|
)
|
||||||
|
await storage.createBucket(
|
||||||
|
`${process.env.GCS_PUBLIC_FILES_BUCKET_NAME}-deleted`
|
||||||
|
)
|
||||||
|
await storage.createBucket(
|
||||||
|
`${process.env.GCS_TEMPLATE_FILES_BUCKET_NAME}-deleted`
|
||||||
|
)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -79,7 +71,7 @@ describe('Filestore', function() {
|
||||||
// retrieve previous metrics from the app
|
// retrieve previous metrics from the app
|
||||||
if (['s3', 'gcs'].includes(Settings.filestore.backend)) {
|
if (['s3', 'gcs'].includes(Settings.filestore.backend)) {
|
||||||
metricPrefix = Settings.filestore.backend
|
metricPrefix = Settings.filestore.backend
|
||||||
previousEgress = await getMetric(
|
previousEgress = await TestHelper.getMetric(
|
||||||
filestoreUrl,
|
filestoreUrl,
|
||||||
`${metricPrefix}_egress`
|
`${metricPrefix}_egress`
|
||||||
)
|
)
|
||||||
|
@ -129,7 +121,7 @@ describe('Filestore', function() {
|
||||||
// The content hash validation might require a full download
|
// The content hash validation might require a full download
|
||||||
// in case the ETag field of the upload response is not a md5 sum.
|
// in case the ETag field of the upload response is not a md5 sum.
|
||||||
if (['s3', 'gcs'].includes(Settings.filestore.backend)) {
|
if (['s3', 'gcs'].includes(Settings.filestore.backend)) {
|
||||||
previousIngress = await getMetric(
|
previousIngress = await TestHelper.getMetric(
|
||||||
filestoreUrl,
|
filestoreUrl,
|
||||||
`${metricPrefix}_ingress`
|
`${metricPrefix}_ingress`
|
||||||
)
|
)
|
||||||
|
@ -223,7 +215,7 @@ describe('Filestore', function() {
|
||||||
|
|
||||||
if (['S3Persistor', 'GcsPersistor'].includes(backend)) {
|
if (['S3Persistor', 'GcsPersistor'].includes(backend)) {
|
||||||
it('should record an egress metric for the upload', async function() {
|
it('should record an egress metric for the upload', async function() {
|
||||||
const metric = await getMetric(
|
const metric = await TestHelper.getMetric(
|
||||||
filestoreUrl,
|
filestoreUrl,
|
||||||
`${metricPrefix}_egress`
|
`${metricPrefix}_egress`
|
||||||
)
|
)
|
||||||
|
@ -232,7 +224,7 @@ describe('Filestore', function() {
|
||||||
|
|
||||||
it('should record an ingress metric when downloading the file', async function() {
|
it('should record an ingress metric when downloading the file', async function() {
|
||||||
await rp.get(fileUrl)
|
await rp.get(fileUrl)
|
||||||
const metric = await getMetric(
|
const metric = await TestHelper.getMetric(
|
||||||
filestoreUrl,
|
filestoreUrl,
|
||||||
`${metricPrefix}_ingress`
|
`${metricPrefix}_ingress`
|
||||||
)
|
)
|
||||||
|
@ -249,7 +241,7 @@ describe('Filestore', function() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
await rp.get(options)
|
await rp.get(options)
|
||||||
const metric = await getMetric(
|
const metric = await TestHelper.getMetric(
|
||||||
filestoreUrl,
|
filestoreUrl,
|
||||||
`${metricPrefix}_ingress`
|
`${metricPrefix}_ingress`
|
||||||
)
|
)
|
||||||
|
@ -394,50 +386,54 @@ describe('Filestore', function() {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (backend === 'GcsPersistor') {
|
||||||
|
describe('when deleting a file in GCS', function() {
|
||||||
|
let fileId, fileUrl, content, error
|
||||||
|
|
||||||
|
beforeEach(async function() {
|
||||||
|
fileId = ObjectId()
|
||||||
|
fileUrl = `${filestoreUrl}/project/${projectId}/file/${fileId}`
|
||||||
|
|
||||||
|
content = '_wombat_' + Math.random()
|
||||||
|
|
||||||
|
const writeStream = request.post(fileUrl)
|
||||||
|
const readStream = streamifier.createReadStream(content)
|
||||||
|
// hack to consume the result to ensure the http request has been fully processed
|
||||||
|
const resultStream = fs.createWriteStream('/dev/null')
|
||||||
|
|
||||||
|
try {
|
||||||
|
await pipeline(readStream, writeStream, resultStream)
|
||||||
|
await rp.delete(fileUrl)
|
||||||
|
} catch (err) {
|
||||||
|
error = err
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should not throw an error', function() {
|
||||||
|
expect(error).not.to.exist
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should copy the file to the deleted-files bucket', async function() {
|
||||||
|
await TestHelper.expectPersistorToHaveFile(
|
||||||
|
app.persistor,
|
||||||
|
`${Settings.filestore.stores.user_files}-deleted`,
|
||||||
|
`${projectId}/${fileId}`,
|
||||||
|
content
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should remove the file from the original bucket', async function() {
|
||||||
|
await TestHelper.expectPersistorNotToHaveFile(
|
||||||
|
app.persistor,
|
||||||
|
Settings.filestore.stores.user_files,
|
||||||
|
`${projectId}/${fileId}`
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
if (BackendSettings[backend].fallback) {
|
if (BackendSettings[backend].fallback) {
|
||||||
describe('with a fallback', function() {
|
describe('with a fallback', function() {
|
||||||
async function uploadStringToPersistor(
|
|
||||||
persistor,
|
|
||||||
bucket,
|
|
||||||
key,
|
|
||||||
content
|
|
||||||
) {
|
|
||||||
const fileStream = streamifier.createReadStream(content)
|
|
||||||
await persistor.promises.sendStream(bucket, key, fileStream)
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getStringFromPersistor(persistor, bucket, key) {
|
|
||||||
const stream = await persistor.promises.getFileStream(
|
|
||||||
bucket,
|
|
||||||
key,
|
|
||||||
{}
|
|
||||||
)
|
|
||||||
return streamToString(stream)
|
|
||||||
}
|
|
||||||
|
|
||||||
async function expectPersistorToHaveFile(
|
|
||||||
persistor,
|
|
||||||
bucket,
|
|
||||||
key,
|
|
||||||
content
|
|
||||||
) {
|
|
||||||
const foundContent = await getStringFromPersistor(
|
|
||||||
persistor,
|
|
||||||
bucket,
|
|
||||||
key
|
|
||||||
)
|
|
||||||
expect(foundContent).to.equal(content)
|
|
||||||
}
|
|
||||||
|
|
||||||
async function expectPersistorNotToHaveFile(persistor, bucket, key) {
|
|
||||||
await expect(
|
|
||||||
getStringFromPersistor(persistor, bucket, key)
|
|
||||||
).to.eventually.have.been.rejected.with.property(
|
|
||||||
'name',
|
|
||||||
'NotFoundError'
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
let constantFileContent,
|
let constantFileContent,
|
||||||
fileId,
|
fileId,
|
||||||
fileKey,
|
fileKey,
|
||||||
|
@ -457,7 +453,7 @@ describe('Filestore', function() {
|
||||||
|
|
||||||
describe('with a file in the fallback bucket', function() {
|
describe('with a file in the fallback bucket', function() {
|
||||||
beforeEach(async function() {
|
beforeEach(async function() {
|
||||||
await uploadStringToPersistor(
|
await TestHelper.uploadStringToPersistor(
|
||||||
app.persistor.fallbackPersistor,
|
app.persistor.fallbackPersistor,
|
||||||
fallbackBucket,
|
fallbackBucket,
|
||||||
fileKey,
|
fileKey,
|
||||||
|
@ -466,7 +462,7 @@ describe('Filestore', function() {
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should not find file in the primary', async function() {
|
it('should not find file in the primary', async function() {
|
||||||
await expectPersistorNotToHaveFile(
|
await TestHelper.expectPersistorNotToHaveFile(
|
||||||
app.persistor.primaryPersistor,
|
app.persistor.primaryPersistor,
|
||||||
bucket,
|
bucket,
|
||||||
fileKey
|
fileKey
|
||||||
|
@ -474,7 +470,7 @@ describe('Filestore', function() {
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should find the file in the fallback', async function() {
|
it('should find the file in the fallback', async function() {
|
||||||
await expectPersistorToHaveFile(
|
await TestHelper.expectPersistorToHaveFile(
|
||||||
app.persistor.fallbackPersistor,
|
app.persistor.fallbackPersistor,
|
||||||
fallbackBucket,
|
fallbackBucket,
|
||||||
fileKey,
|
fileKey,
|
||||||
|
@ -495,7 +491,7 @@ describe('Filestore', function() {
|
||||||
it('should not copy the file to the primary', async function() {
|
it('should not copy the file to the primary', async function() {
|
||||||
await rp.get(fileUrl)
|
await rp.get(fileUrl)
|
||||||
|
|
||||||
await expectPersistorNotToHaveFile(
|
await TestHelper.expectPersistorNotToHaveFile(
|
||||||
app.persistor.primaryPersistor,
|
app.persistor.primaryPersistor,
|
||||||
bucket,
|
bucket,
|
||||||
fileKey
|
fileKey
|
||||||
|
@ -518,7 +514,7 @@ describe('Filestore', function() {
|
||||||
// wait for the file to copy in the background
|
// wait for the file to copy in the background
|
||||||
await promisify(setTimeout)(1000)
|
await promisify(setTimeout)(1000)
|
||||||
|
|
||||||
await expectPersistorToHaveFile(
|
await TestHelper.expectPersistorToHaveFile(
|
||||||
app.persistor.primaryPersistor,
|
app.persistor.primaryPersistor,
|
||||||
bucket,
|
bucket,
|
||||||
fileKey,
|
fileKey,
|
||||||
|
@ -557,7 +553,7 @@ describe('Filestore', function() {
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should leave the old file in the old bucket', async function() {
|
it('should leave the old file in the old bucket', async function() {
|
||||||
await expectPersistorToHaveFile(
|
await TestHelper.expectPersistorToHaveFile(
|
||||||
app.persistor.fallbackPersistor,
|
app.persistor.fallbackPersistor,
|
||||||
fallbackBucket,
|
fallbackBucket,
|
||||||
fileKey,
|
fileKey,
|
||||||
|
@ -566,7 +562,7 @@ describe('Filestore', function() {
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should not create a new file in the old bucket', async function() {
|
it('should not create a new file in the old bucket', async function() {
|
||||||
await expectPersistorNotToHaveFile(
|
await TestHelper.expectPersistorNotToHaveFile(
|
||||||
app.persistor.fallbackPersistor,
|
app.persistor.fallbackPersistor,
|
||||||
fallbackBucket,
|
fallbackBucket,
|
||||||
newFileKey
|
newFileKey
|
||||||
|
@ -574,7 +570,7 @@ describe('Filestore', function() {
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should create a new file in the new bucket', async function() {
|
it('should create a new file in the new bucket', async function() {
|
||||||
await expectPersistorToHaveFile(
|
await TestHelper.expectPersistorToHaveFile(
|
||||||
app.persistor.primaryPersistor,
|
app.persistor.primaryPersistor,
|
||||||
bucket,
|
bucket,
|
||||||
newFileKey,
|
newFileKey,
|
||||||
|
@ -586,7 +582,7 @@ describe('Filestore', function() {
|
||||||
// wait for the file to copy in the background
|
// wait for the file to copy in the background
|
||||||
await promisify(setTimeout)(1000)
|
await promisify(setTimeout)(1000)
|
||||||
|
|
||||||
await expectPersistorNotToHaveFile(
|
await TestHelper.expectPersistorNotToHaveFile(
|
||||||
app.persistor.primaryPersistor,
|
app.persistor.primaryPersistor,
|
||||||
bucket,
|
bucket,
|
||||||
fileKey
|
fileKey
|
||||||
|
@ -603,7 +599,7 @@ describe('Filestore', function() {
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should leave the old file in the old bucket', async function() {
|
it('should leave the old file in the old bucket', async function() {
|
||||||
await expectPersistorToHaveFile(
|
await TestHelper.expectPersistorToHaveFile(
|
||||||
app.persistor.fallbackPersistor,
|
app.persistor.fallbackPersistor,
|
||||||
fallbackBucket,
|
fallbackBucket,
|
||||||
fileKey,
|
fileKey,
|
||||||
|
@ -612,7 +608,7 @@ describe('Filestore', function() {
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should not create a new file in the old bucket', async function() {
|
it('should not create a new file in the old bucket', async function() {
|
||||||
await expectPersistorNotToHaveFile(
|
await TestHelper.expectPersistorNotToHaveFile(
|
||||||
app.persistor.fallbackPersistor,
|
app.persistor.fallbackPersistor,
|
||||||
fallbackBucket,
|
fallbackBucket,
|
||||||
newFileKey
|
newFileKey
|
||||||
|
@ -620,7 +616,7 @@ describe('Filestore', function() {
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should create a new file in the new bucket', async function() {
|
it('should create a new file in the new bucket', async function() {
|
||||||
await expectPersistorToHaveFile(
|
await TestHelper.expectPersistorToHaveFile(
|
||||||
app.persistor.primaryPersistor,
|
app.persistor.primaryPersistor,
|
||||||
bucket,
|
bucket,
|
||||||
newFileKey,
|
newFileKey,
|
||||||
|
@ -632,7 +628,7 @@ describe('Filestore', function() {
|
||||||
// wait for the file to copy in the background
|
// wait for the file to copy in the background
|
||||||
await promisify(setTimeout)(1000)
|
await promisify(setTimeout)(1000)
|
||||||
|
|
||||||
await expectPersistorToHaveFile(
|
await TestHelper.expectPersistorToHaveFile(
|
||||||
app.persistor.primaryPersistor,
|
app.persistor.primaryPersistor,
|
||||||
bucket,
|
bucket,
|
||||||
fileKey,
|
fileKey,
|
||||||
|
@ -655,7 +651,7 @@ describe('Filestore', function() {
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should store the file on the primary', async function() {
|
it('should store the file on the primary', async function() {
|
||||||
await expectPersistorToHaveFile(
|
await TestHelper.expectPersistorToHaveFile(
|
||||||
app.persistor.primaryPersistor,
|
app.persistor.primaryPersistor,
|
||||||
bucket,
|
bucket,
|
||||||
fileKey,
|
fileKey,
|
||||||
|
@ -664,7 +660,7 @@ describe('Filestore', function() {
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should not store the file on the fallback', async function() {
|
it('should not store the file on the fallback', async function() {
|
||||||
await expectPersistorNotToHaveFile(
|
await TestHelper.expectPersistorNotToHaveFile(
|
||||||
app.persistor.fallbackPersistor,
|
app.persistor.fallbackPersistor,
|
||||||
fallbackBucket,
|
fallbackBucket,
|
||||||
`${projectId}/${fileId}`
|
`${projectId}/${fileId}`
|
||||||
|
@ -675,7 +671,7 @@ describe('Filestore', function() {
|
||||||
describe('when deleting a file', function() {
|
describe('when deleting a file', function() {
|
||||||
describe('when the file exists on the primary', function() {
|
describe('when the file exists on the primary', function() {
|
||||||
beforeEach(async function() {
|
beforeEach(async function() {
|
||||||
await uploadStringToPersistor(
|
await TestHelper.uploadStringToPersistor(
|
||||||
app.persistor.primaryPersistor,
|
app.persistor.primaryPersistor,
|
||||||
bucket,
|
bucket,
|
||||||
fileKey,
|
fileKey,
|
||||||
|
@ -694,7 +690,7 @@ describe('Filestore', function() {
|
||||||
|
|
||||||
describe('when the file exists on the fallback', function() {
|
describe('when the file exists on the fallback', function() {
|
||||||
beforeEach(async function() {
|
beforeEach(async function() {
|
||||||
await uploadStringToPersistor(
|
await TestHelper.uploadStringToPersistor(
|
||||||
app.persistor.fallbackPersistor,
|
app.persistor.fallbackPersistor,
|
||||||
fallbackBucket,
|
fallbackBucket,
|
||||||
fileKey,
|
fileKey,
|
||||||
|
@ -713,13 +709,13 @@ describe('Filestore', function() {
|
||||||
|
|
||||||
describe('when the file exists on both the primary and the fallback', function() {
|
describe('when the file exists on both the primary and the fallback', function() {
|
||||||
beforeEach(async function() {
|
beforeEach(async function() {
|
||||||
await uploadStringToPersistor(
|
await TestHelper.uploadStringToPersistor(
|
||||||
app.persistor.primaryPersistor,
|
app.persistor.primaryPersistor,
|
||||||
bucket,
|
bucket,
|
||||||
fileKey,
|
fileKey,
|
||||||
constantFileContent
|
constantFileContent
|
||||||
)
|
)
|
||||||
await uploadStringToPersistor(
|
await TestHelper.uploadStringToPersistor(
|
||||||
app.persistor.fallbackPersistor,
|
app.persistor.fallbackPersistor,
|
||||||
fallbackBucket,
|
fallbackBucket,
|
||||||
fileKey,
|
fileKey,
|
||||||
|
@ -773,7 +769,7 @@ describe('Filestore', function() {
|
||||||
|
|
||||||
if (['S3Persistor', 'GcsPersistor'].includes(backend)) {
|
if (['S3Persistor', 'GcsPersistor'].includes(backend)) {
|
||||||
it('should record an egress metric for the upload', async function() {
|
it('should record an egress metric for the upload', async function() {
|
||||||
const metric = await getMetric(
|
const metric = await TestHelper.getMetric(
|
||||||
filestoreUrl,
|
filestoreUrl,
|
||||||
`${metricPrefix}_egress`
|
`${metricPrefix}_egress`
|
||||||
)
|
)
|
||||||
|
|
|
@ -21,10 +21,14 @@ function s3Stores() {
|
||||||
|
|
||||||
function gcsConfig() {
|
function gcsConfig() {
|
||||||
return {
|
return {
|
||||||
apiEndpoint: process.env.GCS_API_ENDPOINT,
|
endpoint: {
|
||||||
apiScheme: process.env.GCS_API_SCHEME,
|
apiEndpoint: process.env.GCS_API_ENDPOINT,
|
||||||
projectId: 'fake',
|
apiScheme: process.env.GCS_API_SCHEME,
|
||||||
directoryKeyRegex: new RegExp('^[0-9a-fA-F]{24}/[0-9a-fA-F]{24}')
|
projectId: 'fake'
|
||||||
|
},
|
||||||
|
directoryKeyRegex: new RegExp('^[0-9a-fA-F]{24}/[0-9a-fA-F]{24}'),
|
||||||
|
unlockBeforeDelete: false, // fake-gcs does not support this
|
||||||
|
deletedBucketSuffix: '-deleted'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
54
services/filestore/test/acceptance/js/TestHelper.js
Normal file
54
services/filestore/test/acceptance/js/TestHelper.js
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
const streamifier = require('streamifier')
|
||||||
|
const rp = require('request-promise-native').defaults({
|
||||||
|
resolveWithFullResponse: true
|
||||||
|
})
|
||||||
|
|
||||||
|
const { expect } = require('chai')
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
uploadStringToPersistor,
|
||||||
|
getStringFromPersistor,
|
||||||
|
expectPersistorToHaveFile,
|
||||||
|
expectPersistorNotToHaveFile,
|
||||||
|
streamToString,
|
||||||
|
getMetric
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getMetric(filestoreUrl, metric) {
|
||||||
|
const res = await rp.get(`${filestoreUrl}/metrics`)
|
||||||
|
expect(res.statusCode).to.equal(200)
|
||||||
|
const metricRegex = new RegExp(`^${metric}{[^}]+} ([0-9]+)$`, 'm')
|
||||||
|
const found = metricRegex.exec(res.body)
|
||||||
|
return parseInt(found ? found[1] : 0) || 0
|
||||||
|
}
|
||||||
|
|
||||||
|
function streamToString(stream) {
|
||||||
|
const chunks = []
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
stream.on('data', chunk => chunks.push(chunk))
|
||||||
|
stream.on('error', reject)
|
||||||
|
stream.on('end', () => resolve(Buffer.concat(chunks).toString('utf8')))
|
||||||
|
stream.resume()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async function uploadStringToPersistor(persistor, bucket, key, content) {
|
||||||
|
const fileStream = streamifier.createReadStream(content)
|
||||||
|
await persistor.promises.sendStream(bucket, key, fileStream)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getStringFromPersistor(persistor, bucket, key) {
|
||||||
|
const stream = await persistor.promises.getFileStream(bucket, key, {})
|
||||||
|
return streamToString(stream)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function expectPersistorToHaveFile(persistor, bucket, key, content) {
|
||||||
|
const foundContent = await getStringFromPersistor(persistor, bucket, key)
|
||||||
|
expect(foundContent).to.equal(content)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function expectPersistorNotToHaveFile(persistor, bucket, key) {
|
||||||
|
await expect(
|
||||||
|
getStringFromPersistor(persistor, bucket, key)
|
||||||
|
).to.eventually.have.been.rejected.with.property('name', 'NotFoundError')
|
||||||
|
}
|
|
@ -88,8 +88,7 @@ describe('GcsPersistorTests', function() {
|
||||||
|
|
||||||
GcsBucket = {
|
GcsBucket = {
|
||||||
file: sinon.stub().returns(GcsFile),
|
file: sinon.stub().returns(GcsFile),
|
||||||
getFiles: sinon.stub().resolves([files]),
|
getFiles: sinon.stub().resolves([files])
|
||||||
deleteFiles: sinon.stub().resolves()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Storage = class {
|
Storage = class {
|
||||||
|
@ -523,20 +522,23 @@ describe('GcsPersistorTests', function() {
|
||||||
return GcsPersistor.promises.deleteDirectory(bucket, directoryName)
|
return GcsPersistor.promises.deleteDirectory(bucket, directoryName)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should delete the objects in the directory', function() {
|
it('should list the objects in the directory', function() {
|
||||||
expect(Storage.prototype.bucket).to.have.been.calledWith(bucket)
|
expect(Storage.prototype.bucket).to.have.been.calledWith(bucket)
|
||||||
expect(GcsBucket.deleteFiles).to.have.been.calledWith({
|
expect(GcsBucket.getFiles).to.have.been.calledWith({
|
||||||
directory: directoryName,
|
directory: directoryName
|
||||||
force: true
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('should delete the files', function() {
|
||||||
|
expect(GcsFile.delete).to.have.been.calledTwice
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('when there is an error deleting the objects', function() {
|
describe('when there is an error listing the objects', function() {
|
||||||
let error
|
let error
|
||||||
|
|
||||||
beforeEach(async function() {
|
beforeEach(async function() {
|
||||||
GcsBucket.deleteFiles = sinon.stub().rejects(genericError)
|
GcsBucket.getFiles = sinon.stub().rejects(genericError)
|
||||||
try {
|
try {
|
||||||
await GcsPersistor.promises.deleteDirectory(bucket, directoryName)
|
await GcsPersistor.promises.deleteDirectory(bucket, directoryName)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|
Loading…
Reference in a new issue