mirror of
https://github.com/overleaf/overleaf.git
synced 2024-11-14 20:40:17 -05:00
Initial open source commit
This commit is contained in:
commit
3199aad601
21 changed files with 1421 additions and 0 deletions
61
services/filestore/.gitignore
vendored
Normal file
61
services/filestore/.gitignore
vendored
Normal file
|
@ -0,0 +1,61 @@
|
|||
compileFolder
|
||||
|
||||
Compiled source #
|
||||
###################
|
||||
*.com
|
||||
*.class
|
||||
*.dll
|
||||
*.exe
|
||||
*.o
|
||||
*.so
|
||||
|
||||
# Packages #
|
||||
############
|
||||
# it's better to unpack these files and commit the raw source
|
||||
# git has its own built in compression methods
|
||||
*.7z
|
||||
*.dmg
|
||||
*.gz
|
||||
*.iso
|
||||
*.jar
|
||||
*.rar
|
||||
*.tar
|
||||
*.zip
|
||||
|
||||
# Logs and databases #
|
||||
######################
|
||||
*.log
|
||||
*.sql
|
||||
*.sqlite
|
||||
|
||||
# OS generated files #
|
||||
######################
|
||||
.DS_Store?
|
||||
ehthumbs.db
|
||||
Icon?
|
||||
Thumbs.db
|
||||
|
||||
/node_modules/*
|
||||
test/IntergrationTests/js/*
|
||||
data/*/*
|
||||
|
||||
app.js
|
||||
app/js/*
|
||||
test/IntergrationTests/js/*
|
||||
test/UnitTests/js/*
|
||||
cookies.txt
|
||||
uploads/*
|
||||
public/js/editor.js
|
||||
public/js/home.js
|
||||
public/js/forms.js
|
||||
public/js/gui.js
|
||||
public/js/admin.js
|
||||
public/stylesheets/mainStyle.css
|
||||
public/minjs/
|
||||
test/unit/js/
|
||||
test/acceptence/js
|
||||
|
||||
**.swp
|
||||
|
||||
/log.json
|
||||
hash_folder
|
64
services/filestore/GruntFile.coffee
Normal file
64
services/filestore/GruntFile.coffee
Normal file
|
@ -0,0 +1,64 @@
|
|||
module.exports = (grunt) ->
|
||||
|
||||
# Project configuration.
|
||||
grunt.initConfig
|
||||
coffee:
|
||||
server:
|
||||
expand: true,
|
||||
flatten: false,
|
||||
cwd: 'app/coffee',
|
||||
src: ['**/*.coffee'],
|
||||
dest: 'app/js/',
|
||||
ext: '.js'
|
||||
|
||||
app_server:
|
||||
expand: true,
|
||||
flatten: false,
|
||||
src: ['app.coffee'],
|
||||
dest: './',
|
||||
ext: '.js'
|
||||
|
||||
server_tests:
|
||||
expand: true,
|
||||
flatten: false,
|
||||
cwd: 'test/unit/coffee',
|
||||
src: ['*.coffee', '**/*.coffee'],
|
||||
dest: 'test/unit/js/',
|
||||
ext: '.js'
|
||||
|
||||
watch:
|
||||
server_coffee:
|
||||
files: ['app/*.coffee','app/**/*.coffee', 'test/unit/coffee/**/*.coffee', 'test/unit/coffee/*.coffee', "app.coffee"]
|
||||
tasks: ["clean", 'coffee', 'mochaTest']
|
||||
|
||||
clean: ["app/js", "test/unit/js", "app.js"]
|
||||
|
||||
nodemon:
|
||||
dev:
|
||||
options:
|
||||
file: 'app.js'
|
||||
|
||||
concurrent:
|
||||
dev:
|
||||
tasks: ['nodemon', 'watch']
|
||||
options:
|
||||
logConcurrentOutput: true
|
||||
|
||||
mochaTest:
|
||||
test:
|
||||
options:
|
||||
reporter: process.env.MOCHA_RUNNER || "spec"
|
||||
src: ['test/*.js', 'test/**/*.js']
|
||||
|
||||
grunt.loadNpmTasks 'grunt-contrib-coffee'
|
||||
grunt.loadNpmTasks 'grunt-contrib-watch'
|
||||
grunt.loadNpmTasks 'grunt-nodemon'
|
||||
grunt.loadNpmTasks 'grunt-contrib-clean'
|
||||
grunt.loadNpmTasks 'grunt-concurrent'
|
||||
grunt.loadNpmTasks 'grunt-mocha-test'
|
||||
|
||||
grunt.registerTask "ci", ["coffee", "mochaTest"]
|
||||
grunt.registerTask 'default', ['coffee', 'concurrent']
|
||||
|
||||
grunt.registerTask "install", "coffee"
|
||||
|
102
services/filestore/app.coffee
Normal file
102
services/filestore/app.coffee
Normal file
|
@ -0,0 +1,102 @@
|
|||
express = require('express')
|
||||
logger = require('logger-sharelatex')
|
||||
logger.initialize("filestore")
|
||||
metrics = require("./app/js/metrics")
|
||||
settings = require("settings-sharelatex")
|
||||
request = require("request")
|
||||
fileController = require("./app/js/FileController")
|
||||
keyBuilder = require("./app/js/KeyBuilder")
|
||||
domain = require("domain")
|
||||
appIsOk = true
|
||||
app = express()
|
||||
streamBuffers = require("stream-buffers")
|
||||
|
||||
|
||||
app.configure ->
|
||||
app.use express.bodyParser()
|
||||
|
||||
app.configure 'development', ->
|
||||
console.log "Development Enviroment"
|
||||
app.use express.errorHandler({ dumpExceptions: true, showStack: true })
|
||||
|
||||
app.configure 'production', ->
|
||||
console.log "Production Enviroment"
|
||||
app.use express.logger()
|
||||
app.use express.errorHandler()
|
||||
|
||||
metrics.inc "startup"
|
||||
|
||||
app.use (req, res, next)->
|
||||
metrics.inc "http-request"
|
||||
next()
|
||||
|
||||
app.use (req, res, next) ->
|
||||
requestDomain = domain.create()
|
||||
requestDomain.add req
|
||||
requestDomain.add res
|
||||
requestDomain.on "error", (err)->
|
||||
res.send 500
|
||||
logger = require('logger-sharelatex')
|
||||
req =
|
||||
body:req.body
|
||||
headers:req.headers
|
||||
url:req.url
|
||||
key: req.key
|
||||
statusCode: req.statusCode
|
||||
err.domainEmitter.res = "to big to log"
|
||||
logger.err err:err, req:req, res:res, "uncaught exception thrown on request"
|
||||
appIsOk = false
|
||||
exit = ->
|
||||
console.log "exit"
|
||||
process.exit(1)
|
||||
setTimeout exit, 20000
|
||||
requestDomain.run next
|
||||
|
||||
app.get "/project/:project_id/file/:file_id", keyBuilder.userFileKey, fileController.getFile
|
||||
app.post "/project/:project_id/file/:file_id", keyBuilder.userFileKey, fileController.insertFile
|
||||
|
||||
app.put "/project/:project_id/file/:file_id", keyBuilder.userFileKey, fileController.copyFile
|
||||
app.del "/project/:project_id/file/:file_id", keyBuilder.userFileKey, fileController.deleteFile
|
||||
|
||||
app.get "/template/:template_id/v/:version/:format", keyBuilder.templateFileKey, fileController.getFile
|
||||
app.post "/template/:template_id/v/:version/:format", keyBuilder.templateFileKey, fileController.insertFile
|
||||
|
||||
app.post "/shutdown", (req, res)->
|
||||
appIsOk = false
|
||||
res.send()
|
||||
|
||||
app.get '/status', (req, res)->
|
||||
if appIsOk
|
||||
res.send('filestore sharelatex up')
|
||||
else
|
||||
logger.log "app is not ok - shutting down"
|
||||
res.send("server is being shut down", 500)
|
||||
|
||||
app.get "/health_check", (req, res)->
|
||||
req.params.project_id = settings.health_check.project_id
|
||||
req.params.file_id = settings.health_check.file_id
|
||||
myWritableStreamBuffer = new streamBuffers.WritableStreamBuffer(initialSize: 100)
|
||||
keyBuilder.userFileKey req, res, ->
|
||||
fileController.getFile req, myWritableStreamBuffer
|
||||
myWritableStreamBuffer.on "close", ->
|
||||
if myWritableStreamBuffer.size() > 0
|
||||
res.send(200)
|
||||
else
|
||||
res.send(503)
|
||||
|
||||
|
||||
|
||||
app.get '*', (req, res)->
|
||||
res.send 404
|
||||
|
||||
serverDomain = domain.create()
|
||||
serverDomain.run ->
|
||||
server = require('http').createServer(app)
|
||||
port = settings.internal.filestore.port or 3009
|
||||
host = settings.internal.filestore.host or "localhost"
|
||||
server.listen port, host, ->
|
||||
logger.log("filestore store listening on #{host}:#{port}")
|
||||
|
||||
serverDomain.on "error", (err)->
|
||||
logger.log err:err, "top level uncaught exception"
|
||||
|
59
services/filestore/app/coffee/FileController.coffee
Normal file
59
services/filestore/app/coffee/FileController.coffee
Normal file
|
@ -0,0 +1,59 @@
|
|||
s3Wrapper = require("./s3Wrapper")
|
||||
settings = require("settings-sharelatex")
|
||||
logger = require("logger-sharelatex")
|
||||
FileHandler = require("./FileHandler")
|
||||
LocalFileWriter = require("./LocalFileWriter")
|
||||
metrics = require("./metrics")
|
||||
oneDayInSeconds = 60 * 60 * 24
|
||||
|
||||
module.exports =
|
||||
|
||||
getFile: (req, res)->
|
||||
metrics.inc "getFile"
|
||||
{key, bucket} = req
|
||||
{format, style} = req.query
|
||||
logger.log key:key, bucket:bucket, format:format, style:style, "reciving request to get file"
|
||||
FileHandler.getFile bucket, key, {format:format,style:style}, (err, fileStream)->
|
||||
if err?
|
||||
logger.err err:err, key:key, bucket:bucket, format:format, style:style, "problem getting file"
|
||||
res.send 500
|
||||
else if req.params.cacheWarm
|
||||
logger.log key:key, bucket:bucket, format:format, style:style, "request is only for cache warm so not sending stream"
|
||||
res.send 200
|
||||
else
|
||||
logger.log key:key, bucket:bucket, format:format, style:style, "sending file to response"
|
||||
fileStream.pipe res
|
||||
|
||||
insertFile: (req, res)->
|
||||
metrics.inc "insertFile"
|
||||
{key, bucket} = req
|
||||
logger.log key:key, bucket:bucket, "reciving request to insert file"
|
||||
FileHandler.insertFile bucket, key, req, (err)->
|
||||
res.send 200
|
||||
|
||||
copyFile: (req, res)->
|
||||
metrics.inc "copyFile"
|
||||
{key, bucket} = req
|
||||
oldProject_id = req.body.source.project_id
|
||||
oldFile_id = req.body.source.file_id
|
||||
logger.log key:key, bucket:bucket, oldProject_id:oldProject_id, oldFile_id:oldFile_id, "reciving request to copy file"
|
||||
s3Wrapper.copyFile bucket, "#{oldProject_id}/#{oldFile_id}", key, (err)->
|
||||
if err?
|
||||
logger.log err:err, oldProject_id:oldProject_id, oldFile_id:oldFile_id, "something went wrong copying file in s3Wrapper"
|
||||
res.send 500
|
||||
else
|
||||
res.send 200
|
||||
|
||||
deleteFile: (req, res)->
|
||||
metrics.inc "deleteFile"
|
||||
{key, bucket} = req
|
||||
logger.log key:key, bucket:bucket, "reciving request to delete file"
|
||||
FileHandler.deleteFile bucket, key, (err)->
|
||||
if err?
|
||||
logger.log err:err, key:key, bucket:bucket, "something went wrong deleting file in s3Wrapper"
|
||||
res.send 500
|
||||
else
|
||||
res.send 204
|
||||
|
||||
|
||||
|
49
services/filestore/app/coffee/FileConverter.coffee
Normal file
49
services/filestore/app/coffee/FileConverter.coffee
Normal file
|
@ -0,0 +1,49 @@
|
|||
easyimage = require("easyimage")
|
||||
_ = require("underscore")
|
||||
metrics = require("./metrics")
|
||||
logger = require("logger-sharelatex")
|
||||
|
||||
approvedFormats = ["png"]
|
||||
|
||||
module.exports =
|
||||
|
||||
convert: (sourcePath, requestedFormat, callback)->
|
||||
logger.log sourcePath:sourcePath, requestedFormat:requestedFormat, "converting file format"
|
||||
timer = new metrics.Timer("imageConvert")
|
||||
destPath = "#{sourcePath}.#{requestedFormat}"
|
||||
sourcePath = "#{sourcePath}[0]"
|
||||
if !_.include approvedFormats, requestedFormat
|
||||
err = new Error("invalid format requested")
|
||||
return callback err
|
||||
args =
|
||||
src: sourcePath
|
||||
dst: destPath
|
||||
easyimage.convert args, (err)->
|
||||
timer.done()
|
||||
callback(err, destPath)
|
||||
|
||||
thumbnail: (sourcePath, callback)->
|
||||
logger.log sourcePath:sourcePath, "thumbnail convert file"
|
||||
destPath = "#{sourcePath}.png"
|
||||
sourcePath = "#{sourcePath}[0]"
|
||||
args =
|
||||
src: sourcePath
|
||||
dst: destPath
|
||||
width: 424
|
||||
height: 300
|
||||
args = "convert -flatten -background white -resize 300x -density 300 #{sourcePath} #{destPath}"
|
||||
easyimage.exec args, (err)->
|
||||
callback(err, destPath)
|
||||
|
||||
preview: (sourcePath, callback)->
|
||||
logger.log sourcePath:sourcePath, "preview convert file"
|
||||
destPath = "#{sourcePath}.png"
|
||||
sourcePath = "#{sourcePath}[0]"
|
||||
args =
|
||||
src: sourcePath
|
||||
dst: destPath
|
||||
width: 600
|
||||
height: 849
|
||||
args = "convert -flatten -background white -resize 600x -density 300 #{sourcePath} #{destPath}"
|
||||
easyimage.exec args, (err)->
|
||||
callback(err, destPath)
|
80
services/filestore/app/coffee/FileHandler.coffee
Normal file
80
services/filestore/app/coffee/FileHandler.coffee
Normal file
|
@ -0,0 +1,80 @@
|
|||
settings = require("settings-sharelatex")
|
||||
s3Wrapper = require("./s3Wrapper")
|
||||
LocalFileWriter = require("./LocalFileWriter")
|
||||
logger = require("logger-sharelatex")
|
||||
FileConverter = require("./FileConverter")
|
||||
KeyBuilder = require("./KeyBuilder")
|
||||
async = require("async")
|
||||
ImageOptimiser = require("./ImageOptimiser")
|
||||
|
||||
|
||||
module.exports =
|
||||
|
||||
insertFile: (bucket, key, stream, callback)->
|
||||
convetedKey = KeyBuilder.getConvertedFolderKey(key)
|
||||
s3Wrapper.deleteDirectory bucket, convetedKey, ->
|
||||
s3Wrapper.sendStreamToS3 bucket, key, stream, ->
|
||||
callback()
|
||||
|
||||
deleteFile: (bucket, key, callback)->
|
||||
convetedKey = KeyBuilder.getConvertedFolderKey(bucket, key)
|
||||
async.parallel [
|
||||
(done)-> s3Wrapper.deleteFile bucket, key, done
|
||||
(done)-> s3Wrapper.deleteFile bucket, convetedKey, done
|
||||
], callback
|
||||
|
||||
getFile: (bucket, key, opts = {}, callback)->
|
||||
logger.log bucket:bucket, key:key, opts:opts, "getting file"
|
||||
if !opts.format? and !opts.style?
|
||||
@_getStandardFile bucket, key, opts, callback
|
||||
else
|
||||
@_getConvertedFile bucket, key, opts, callback
|
||||
|
||||
_getStandardFile: (bucket, key, opts, callback)->
|
||||
s3Wrapper.getFileStream bucket, key, (err, fileStream)->
|
||||
if err?
|
||||
logger.err bucket:bucket, key:key, opts:opts, "error getting fileStream"
|
||||
callback err, fileStream
|
||||
|
||||
_getConvertedFile: (bucket, key, opts, callback)->
|
||||
convetedKey = KeyBuilder.addCachingToKey(key, opts)
|
||||
s3Wrapper.checkIfFileExists bucket, convetedKey, (err, exists)=>
|
||||
if exists
|
||||
s3Wrapper.getFileStream bucket, convetedKey, callback
|
||||
else
|
||||
@_getConvertedFileAndCache bucket, key, convetedKey, opts, callback
|
||||
|
||||
_getConvertedFileAndCache: (bucket, key, convetedKey, opts, callback)->
|
||||
@_convertFile bucket, key, opts, (err, fsPath)->
|
||||
if err?
|
||||
logger.err err:err, fsPath:fsPath, bucket:bucket, key:key, opts:opts, "something went wrong with converting file"
|
||||
return callback(err)
|
||||
ImageOptimiser.compressPng fsPath, (err)->
|
||||
if err?
|
||||
logger.err err:err, fsPath:fsPath, bucket:bucket, key:key, opts:opts, "something went wrong optimising png file"
|
||||
return callback(err)
|
||||
s3Wrapper.sendFileToS3 bucket, convetedKey, fsPath, (err)->
|
||||
if err?
|
||||
logger.err err:err, bucket:bucket, key:key, convetedKey:convetedKey, opts:opts, "something went wrong seing file to s3"
|
||||
return callback(err)
|
||||
s3Wrapper.getFileStream bucket, convetedKey, callback
|
||||
|
||||
_convertFile: (bucket, origonalKey, opts, callback)->
|
||||
@_writeS3FileToDisk bucket, origonalKey, (err, origonalFsPath)->
|
||||
if opts.format?
|
||||
FileConverter.convert origonalFsPath, opts.format, callback
|
||||
else if opts.style == "thumbnail"
|
||||
FileConverter.thumbnail origonalFsPath, callback
|
||||
else if opts.style == "preview"
|
||||
FileConverter.preview origonalFsPath, callback
|
||||
else
|
||||
throw new Error("should have specified opts to convert file with #{JSON.stringify(opts)}")
|
||||
|
||||
|
||||
_writeS3FileToDisk: (bucket, key, callback)->
|
||||
s3Wrapper.getFileStream bucket, key, (err, fileStream)->
|
||||
LocalFileWriter.writeStream fileStream, key, callback
|
||||
|
||||
|
||||
|
||||
|
29
services/filestore/app/coffee/ImageOptimiser.coffee
Normal file
29
services/filestore/app/coffee/ImageOptimiser.coffee
Normal file
|
@ -0,0 +1,29 @@
|
|||
PngCrush = require('pngcrush')
|
||||
fs = require("fs")
|
||||
logger = require("logger-sharelatex")
|
||||
|
||||
|
||||
module.exports =
|
||||
|
||||
compressPng: (localPath, callback)->
|
||||
optimisedPath = "#{localPath}-optimised"
|
||||
startTime = new Date()
|
||||
logger.log localPath:localPath, optimisedPath:optimisedPath, "optimising png path"
|
||||
readStream = fs.createReadStream(localPath)
|
||||
writeStream = fs.createWriteStream(optimisedPath)
|
||||
readStream.on "error", (err)->
|
||||
logger.err err:err, localPath:localPath, "something went wrong getting read stream for compressPng"
|
||||
callback(err)
|
||||
writeStream.on "error", (err)->
|
||||
logger.err err:err, localPath:localPath, "something went wrong getting write stream for compressPng"
|
||||
callback(err)
|
||||
myCrusher = new PngCrush()
|
||||
myCrusher.on "error", (err)->
|
||||
logger.err err:err, localPath:localPath, "error compressing file"
|
||||
callback err
|
||||
readStream.pipe(myCrusher).pipe(writeStream)
|
||||
writeStream.on "finish", ->
|
||||
timeTaken = new Date() - startTime
|
||||
logger.log localPath:localPath, timeTaken:timeTaken, "finished converting file"
|
||||
fs.rename optimisedPath, localPath, callback
|
||||
|
34
services/filestore/app/coffee/KeyBuilder.coffee
Normal file
34
services/filestore/app/coffee/KeyBuilder.coffee
Normal file
|
@ -0,0 +1,34 @@
|
|||
settings = require("settings-sharelatex")
|
||||
|
||||
module.exports =
|
||||
|
||||
|
||||
getConvertedFolderKey: (key)->
|
||||
key = "#{key}-converted-cache/"
|
||||
|
||||
addCachingToKey: (key, opts)->
|
||||
key = @getConvertedFolderKey(key)
|
||||
if opts.format? and !opts.style?
|
||||
key = "#{key}format-#{opts.format}"
|
||||
if opts.style? and !opts.format?
|
||||
key = "#{key}style-#{opts.style}"
|
||||
if opts.style? and opts.format?
|
||||
key = "#{key}format-#{opts.format}-style-#{opts.style}"
|
||||
return key
|
||||
|
||||
|
||||
userFileKey: (req, res, next)->
|
||||
{project_id, file_id} = req.params
|
||||
req.key = "#{project_id}/#{file_id}"
|
||||
req.bucket = settings.s3.buckets.user_files
|
||||
next()
|
||||
|
||||
templateFileKey: (req, res, next)->
|
||||
{template_id, format, version} = req.params
|
||||
req.key = "#{template_id}/#{version}/#{format}"
|
||||
req.bucket = settings.s3.buckets.template_files
|
||||
req.version = version
|
||||
opts = req.query
|
||||
next()
|
||||
|
||||
|
36
services/filestore/app/coffee/LocalFileWriter.coffee
Normal file
36
services/filestore/app/coffee/LocalFileWriter.coffee
Normal file
|
@ -0,0 +1,36 @@
|
|||
fs = require("fs")
|
||||
uuid = require('node-uuid')
|
||||
path = require("path")
|
||||
_ = require("underscore")
|
||||
logger = require("logger-sharelatex")
|
||||
metrics = require("./metrics")
|
||||
|
||||
module.exports =
|
||||
|
||||
writeStream: (stream, key, callback)->
|
||||
|
||||
timer = new metrics.Timer("writingFile")
|
||||
callback = _.once callback
|
||||
fsPath = @_getPath(key)
|
||||
logger.log fsPath:fsPath, "writing file locally"
|
||||
writeStream = fs.createWriteStream(fsPath)
|
||||
stream.pipe writeStream
|
||||
writeStream.on "finish", ->
|
||||
timer.done()
|
||||
logger.log fsPath:fsPath, "finished writing file locally"
|
||||
callback(null, fsPath)
|
||||
writeStream.on "error", (err)->
|
||||
logger.err err:err, fsPath:fsPath, "problem writing file locally, with write stream"
|
||||
callback err
|
||||
stream.on "error", (err)->
|
||||
logger.log err:err, fsPath:fsPath, "problem writing file locally, with read stream"
|
||||
callback err
|
||||
|
||||
deleteFile: (fsPath, callback)->
|
||||
fs.unlink fsPath, callback
|
||||
|
||||
_getPath : (key)->
|
||||
if !key?
|
||||
key = uuid.v1()
|
||||
key = key.replace(/\//g,"-")
|
||||
path.join(__dirname, "../../uploads/#{key}")
|
24
services/filestore/app/coffee/metrics.coffee
Normal file
24
services/filestore/app/coffee/metrics.coffee
Normal file
|
@ -0,0 +1,24 @@
|
|||
StatsD = require('lynx')
|
||||
settings = require('settings-sharelatex')
|
||||
statsd = new StatsD('localhost', 8125, {on_error:->})
|
||||
|
||||
buildKey = (key)-> "filestore.#{process.env.NODE_ENV}.#{key}"
|
||||
|
||||
module.exports =
|
||||
set : (key, value, sampleRate = 1)->
|
||||
statsd.set buildKey(key), value, sampleRate
|
||||
|
||||
inc : (key, sampleRate = 1)->
|
||||
statsd.increment buildKey(key), sampleRate
|
||||
|
||||
Timer : class
|
||||
constructor :(key, sampleRate = 1)->
|
||||
this.start = new Date()
|
||||
this.key = buildKey(key)
|
||||
done:->
|
||||
timeSpan = new Date - this.start
|
||||
statsd.timing(this.key, timeSpan, this.sampleRate)
|
||||
|
||||
gauge : (key, value, sampleRate = 1)->
|
||||
statsd.gauge key, value, sampleRate
|
||||
|
102
services/filestore/app/coffee/s3Wrapper.coffee
Normal file
102
services/filestore/app/coffee/s3Wrapper.coffee
Normal file
|
@ -0,0 +1,102 @@
|
|||
settings = require("settings-sharelatex")
|
||||
request = require("request")
|
||||
logger = require("logger-sharelatex")
|
||||
fs = require("fs")
|
||||
knox = require("knox")
|
||||
path = require("path")
|
||||
LocalFileWriter = require("./LocalFileWriter")
|
||||
_ = require("underscore")
|
||||
|
||||
|
||||
thirtySeconds = 30 * 1000
|
||||
|
||||
buildDefaultOptions = (bucketName, method, key)->
|
||||
return {
|
||||
aws:
|
||||
key: settings.s3.key
|
||||
secret: settings.s3.secret
|
||||
bucket: bucketName
|
||||
method: method
|
||||
timeout: thirtySeconds
|
||||
uri:"https://#{bucketName}.s3.amazonaws.com/#{key}"
|
||||
}
|
||||
|
||||
module.exports =
|
||||
|
||||
sendFileToS3: (bucketName, key, fsPath, callback)->
|
||||
s3Client = knox.createClient
|
||||
key: settings.s3.key
|
||||
secret: settings.s3.secret
|
||||
bucket: bucketName
|
||||
putEventEmiter = s3Client.putFile fsPath, key, (err, res)->
|
||||
if err?
|
||||
logger.err err:err, bucketName:bucketName, key:key, fsPath:fsPath,"something went wrong uploading file to s3"
|
||||
return callback(err)
|
||||
if res.statusCode != 200
|
||||
logger.err bucketName:bucketName, key:key, fsPath:fsPath, "non 200 response from s3 putting file"
|
||||
return callback("non 200 response from s3 on put file")
|
||||
LocalFileWriter.deleteFile fsPath, (err)->
|
||||
logger.log res:res, bucketName:bucketName, key:key, fsPath:fsPath,"file uploaded to s3"
|
||||
callback(err)
|
||||
putEventEmiter.on "error", (err)->
|
||||
logger.err err:err, bucketName:bucketName, key:key, fsPath:fsPath, "error emmited on put of file"
|
||||
callback err
|
||||
|
||||
|
||||
sendStreamToS3: (bucketName, key, readStream, callback)->
|
||||
logger.log bucketName:bucketName, key:key, "sending file to s3"
|
||||
readStream.on "error", (err)->
|
||||
logger.err bucketName:bucketName, key:key, "error on stream to send to s3"
|
||||
LocalFileWriter.writeStream readStream, null, (err, fsPath)=>
|
||||
if err?
|
||||
logger.err bucketName:bucketName, key:key, fsPath:fsPath, err:err, "something went wrong writing stream to disk"
|
||||
return callback(err)
|
||||
@sendFileToS3 bucketName, key, fsPath, callback
|
||||
|
||||
getFileStream: (bucketName, key, callback)->
|
||||
logger.log bucketName:bucketName, key:key, "getting file from s3"
|
||||
options = buildDefaultOptions(bucketName, "get", key)
|
||||
readStream = request(options)
|
||||
readStream.on "error", (err)->
|
||||
logger.err bucketName:bucketName, key:key, "error getting file stream from s3"
|
||||
callback null, readStream
|
||||
|
||||
copyFile: (bucketName, sourceKey, destKey, callback)->
|
||||
logger.log bucketName:bucketName, sourceKey:sourceKey, destKey:destKey, "copying file in s3"
|
||||
s3Client = knox.createClient
|
||||
key: settings.s3.key
|
||||
secret: settings.s3.secret
|
||||
bucket: bucketName
|
||||
s3Client.copyFile sourceKey, destKey, (err)->
|
||||
if err?
|
||||
logger.err bucketName:bucketName, sourceKey:sourceKey, destKey:destKey, "something went wrong copying file in aws"
|
||||
callback(err)
|
||||
|
||||
deleteFile: (bucketName, key, callback)->
|
||||
logger.log bucketName:bucketName, key:key, "delete file in s3"
|
||||
options = buildDefaultOptions(bucketName, "delete", key)
|
||||
request options, (err, res)->
|
||||
if err?
|
||||
logger.err res:res, bucketName:bucketName, key:key, "something went wrong deleting file in aws"
|
||||
callback(err)
|
||||
|
||||
deleteDirectory: (bucketName, key, callback)->
|
||||
s3Client = knox.createClient
|
||||
key: settings.s3.key
|
||||
secret: settings.s3.secret
|
||||
bucket: bucketName
|
||||
s3Client.list prefix:key, (err, data)->
|
||||
keys = _.map data.Contents, (entry)->
|
||||
return entry.Key
|
||||
s3Client.deleteMultiple keys, callback
|
||||
|
||||
checkIfFileExists:(bucketName, key, callback)->
|
||||
logger.log bucketName:bucketName, key:key, "checking if file exists in s3"
|
||||
options = buildDefaultOptions(bucketName, "head", key)
|
||||
request options, (err, res)->
|
||||
if err?
|
||||
logger.err res:res, bucketName:bucketName, key:key, "something went wrong copying file in aws"
|
||||
exists = res.statusCode == 200
|
||||
logger.log bucketName:bucketName, key:key, exists:exists, "checked if file exsists in s3"
|
||||
callback(err, exists)
|
||||
|
22
services/filestore/config/settings.development.coffee
Normal file
22
services/filestore/config/settings.development.coffee
Normal file
|
@ -0,0 +1,22 @@
|
|||
module.exports =
|
||||
internal:
|
||||
filestore:
|
||||
port: 3009
|
||||
host: "localhost"
|
||||
|
||||
# ShareLaTeX stores binary files like images in S3.
|
||||
# Fill in your Amazon S3 credentials below.
|
||||
s3:
|
||||
key: ''
|
||||
secret: ''
|
||||
buckets:
|
||||
user_files: ""
|
||||
template_files: ""
|
||||
|
||||
|
||||
# Filestore health check
|
||||
# ----------------------
|
||||
# Project and file details to check in filestore when calling /health_check
|
||||
# health_check:
|
||||
# project_id: ""
|
||||
# file_id: ""
|
32
services/filestore/package.json
Normal file
32
services/filestore/package.json
Normal file
|
@ -0,0 +1,32 @@
|
|||
{
|
||||
"name": "filestore-sharelatex",
|
||||
"version": "0.0.1",
|
||||
"dependencies": {
|
||||
"settings": "git+ssh://git@bitbucket.org:sharelatex/settings-sharelatex.git#master",
|
||||
"logger": "git+ssh://git@bitbucket.org:sharelatex/logger-sharelatex.git#bunyan",
|
||||
"request": "2.14.0",
|
||||
"lynx": "0.0.11",
|
||||
"grunt-mocha-test": "~0.8.2",
|
||||
"knox": "~0.8.8",
|
||||
"node-uuid": "~1.4.1",
|
||||
"underscore": "~1.5.2",
|
||||
"easyimage": "~0.1.6",
|
||||
"express": "~3.4.8",
|
||||
"longjohn": "~0.2.2",
|
||||
"async": "~0.2.10",
|
||||
"pngcrush": "0.0.3",
|
||||
"stream-buffers": "~0.2.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"sinon": "",
|
||||
"chai": "",
|
||||
"sandboxed-module": "",
|
||||
"grunt": "0.4.1",
|
||||
"grunt-contrib-requirejs": "0.4.1",
|
||||
"grunt-contrib-coffee": "0.7.0",
|
||||
"grunt-contrib-watch": "0.5.3",
|
||||
"grunt-nodemon": "0.1.2",
|
||||
"grunt-contrib-clean": "0.5.0",
|
||||
"grunt-concurrent": "0.4.2"
|
||||
}
|
||||
}
|
126
services/filestore/test/unit/coffee/FileControllerTests.coffee
Normal file
126
services/filestore/test/unit/coffee/FileControllerTests.coffee
Normal file
|
@ -0,0 +1,126 @@
|
|||
assert = require("chai").assert
|
||||
sinon = require('sinon')
|
||||
chai = require('chai')
|
||||
should = chai.should()
|
||||
expect = chai.expect
|
||||
modulePath = "../../../app/js/FileController.js"
|
||||
SandboxedModule = require('sandboxed-module')
|
||||
|
||||
describe "FileController", ->
|
||||
|
||||
beforeEach ->
|
||||
@s3Wrapper =
|
||||
sendStreamToS3: sinon.stub()
|
||||
getAndPipe: sinon.stub()
|
||||
copyFile: sinon.stub()
|
||||
deleteFile:sinon.stub()
|
||||
|
||||
@settings =
|
||||
s3:
|
||||
buckets:
|
||||
user_files:"user_files"
|
||||
@FileHandler =
|
||||
getFile: sinon.stub()
|
||||
deleteFile: sinon.stub()
|
||||
insertFile: sinon.stub()
|
||||
@LocalFileWriter = {}
|
||||
@controller = SandboxedModule.require modulePath, requires:
|
||||
"./LocalFileWriter":@LocalFileWriter
|
||||
"./FileHandler": @FileHandler
|
||||
"./s3Wrapper":@s3Wrapper
|
||||
"settings-sharelatex": @settings
|
||||
"logger-sharelatex":
|
||||
log:->
|
||||
err:->
|
||||
@project_id = "project_id"
|
||||
@file_id = "file_id"
|
||||
@bucket = "user_files"
|
||||
@key = "#{@project_id}/#{@file_id}"
|
||||
@req =
|
||||
key:@key
|
||||
bucket:@bucket
|
||||
query:{}
|
||||
params:
|
||||
project_id:@project_id
|
||||
file_id:@file_id
|
||||
@res =
|
||||
setHeader: ->
|
||||
@fileStream = {}
|
||||
|
||||
describe "getFile", ->
|
||||
|
||||
it "should pipe the stream", (done)->
|
||||
@FileHandler.getFile.callsArgWith(3, null, @fileStream)
|
||||
@fileStream.pipe = (res)=>
|
||||
res.should.equal @res
|
||||
done()
|
||||
@controller.getFile @req, @res
|
||||
|
||||
it "should send a 200 if the cacheWarm param is true", (done)->
|
||||
@req.params.cacheWarm = true
|
||||
@FileHandler.getFile.callsArgWith(3, null, @fileStream)
|
||||
@res.send = (statusCode)=>
|
||||
statusCode.should.equal 200
|
||||
done()
|
||||
@controller.getFile @req, @res
|
||||
|
||||
it "should send a 500 if there is a problem", (done)->
|
||||
@FileHandler.getFile.callsArgWith(3, "error")
|
||||
@res.send = (code)=>
|
||||
code.should.equal 500
|
||||
done()
|
||||
@controller.getFile @req, @res
|
||||
|
||||
|
||||
describe "insertFile", ->
|
||||
|
||||
it "should send bucket name key and res to s3Wrapper", (done)->
|
||||
@FileHandler.insertFile.callsArgWith(3)
|
||||
@res.send = =>
|
||||
@FileHandler.insertFile.calledWith(@bucket, @key, @req).should.equal true
|
||||
done()
|
||||
@controller.insertFile @req, @res
|
||||
|
||||
|
||||
describe "copyFile", ->
|
||||
beforeEach ->
|
||||
@oldFile_id = "old_file_id"
|
||||
@oldProject_id = "old_project_id"
|
||||
@req.body =
|
||||
source:
|
||||
project_id: @oldProject_id
|
||||
file_id: @oldFile_id
|
||||
|
||||
it "should send bucket name and both keys to s3Wrapper", (done)->
|
||||
@s3Wrapper.copyFile.callsArgWith(3)
|
||||
@res.send = (code)=>
|
||||
code.should.equal 200
|
||||
@s3Wrapper.copyFile.calledWith(@bucket, "#{@oldProject_id}/#{@oldFile_id}", @key).should.equal true
|
||||
done()
|
||||
@controller.copyFile @req, @res
|
||||
|
||||
|
||||
it "should send a 500 if there was an error", (done)->
|
||||
@s3Wrapper.copyFile.callsArgWith(3, "error")
|
||||
@res.send = (code)=>
|
||||
code.should.equal 500
|
||||
done()
|
||||
@controller.copyFile @req, @res
|
||||
|
||||
|
||||
describe "delete file", ->
|
||||
|
||||
it "should tell the file handler", (done)->
|
||||
@FileHandler.deleteFile.callsArgWith(2)
|
||||
@res.send = (code)=>
|
||||
code.should.equal 204
|
||||
@FileHandler.deleteFile.calledWith(@bucket, @key).should.equal true
|
||||
done()
|
||||
@controller.deleteFile @req, @res
|
||||
|
||||
it "should send a 500 if there was an error", (done)->
|
||||
@FileHandler.deleteFile.callsArgWith(2, "error")
|
||||
@res.send = (code)->
|
||||
code.should.equal 500
|
||||
done()
|
||||
@controller.deleteFile @req, @res
|
|
@ -0,0 +1,73 @@
|
|||
assert = require("chai").assert
|
||||
sinon = require('sinon')
|
||||
chai = require('chai')
|
||||
should = chai.should()
|
||||
expect = chai.expect
|
||||
modulePath = "../../../app/js/FileConverter.js"
|
||||
SandboxedModule = require('sandboxed-module')
|
||||
|
||||
describe "FileConverter", ->
|
||||
|
||||
beforeEach ->
|
||||
|
||||
@easyimage =
|
||||
convert:sinon.stub()
|
||||
exec: sinon.stub()
|
||||
@converter = SandboxedModule.require modulePath, requires:
|
||||
"easyimage":@easyimage
|
||||
"logger-sharelatex":
|
||||
log:->
|
||||
err:->
|
||||
|
||||
@sourcePath = "/this/path/here.eps"
|
||||
@format = "png"
|
||||
@error = "Error"
|
||||
|
||||
describe "convert", ->
|
||||
|
||||
it "should convert the source to the requested format", (done)->
|
||||
@easyimage.convert.callsArgWith(1)
|
||||
@converter.convert @sourcePath, @format, (err)=>
|
||||
args = @easyimage.convert.args[0][0]
|
||||
args.src.should.equal @sourcePath+"[0]"
|
||||
args.dst.should.equal "#{@sourcePath}.#{@format}"
|
||||
done()
|
||||
|
||||
|
||||
it "should return the dest path", (done)->
|
||||
@easyimage.convert.callsArgWith(1)
|
||||
@converter.convert @sourcePath, @format, (err, destPath)=>
|
||||
destPath.should.equal "#{@sourcePath}.#{@format}"
|
||||
done()
|
||||
|
||||
it "should return the error from convert", (done)->
|
||||
@easyimage.convert.callsArgWith(1, @error)
|
||||
@converter.convert @sourcePath, @format, (err)=>
|
||||
err.should.equal @error
|
||||
done()
|
||||
|
||||
it "should not accapt an non aproved format", (done)->
|
||||
@easyimage.convert.callsArgWith(1)
|
||||
@converter.convert @sourcePath, "ahhhhh", (err)=>
|
||||
expect(err).to.exist
|
||||
done()
|
||||
|
||||
|
||||
describe "thumbnail", ->
|
||||
it "should call easy image resize with args", (done)->
|
||||
@easyimage.exec.callsArgWith(1)
|
||||
@converter.thumbnail @sourcePath, (err)=>
|
||||
args = @easyimage.exec.args[0][0]
|
||||
args.indexOf(@sourcePath).should.not.equal -1
|
||||
done()
|
||||
|
||||
it "should compress the png", ()->
|
||||
|
||||
|
||||
describe "preview", ->
|
||||
it "should call easy image resize with args", (done)->
|
||||
@easyimage.exec.callsArgWith(1)
|
||||
@converter.preview @sourcePath, (err)=>
|
||||
args = @easyimage.exec.args[0][0]
|
||||
args.indexOf(@sourcePath).should.not.equal -1
|
||||
done()
|
177
services/filestore/test/unit/coffee/FileHandlerTests.coffee
Normal file
177
services/filestore/test/unit/coffee/FileHandlerTests.coffee
Normal file
|
@ -0,0 +1,177 @@
|
|||
|
||||
assert = require("chai").assert
|
||||
sinon = require('sinon')
|
||||
chai = require('chai')
|
||||
should = chai.should()
|
||||
expect = chai.expect
|
||||
modulePath = "../../../app/js/FileHandler.js"
|
||||
SandboxedModule = require('sandboxed-module')
|
||||
|
||||
describe "FileHandler", ->
|
||||
|
||||
beforeEach ->
|
||||
@settings =
|
||||
s3:
|
||||
buckets:
|
||||
user_files:"user_files"
|
||||
@s3Wrapper =
|
||||
getFileStream: sinon.stub()
|
||||
checkIfFileExists: sinon.stub()
|
||||
deleteFile: sinon.stub()
|
||||
deleteDirectory: sinon.stub()
|
||||
sendStreamToS3: sinon.stub()
|
||||
insertFile: sinon.stub()
|
||||
@LocalFileWriter =
|
||||
writeStream: sinon.stub()
|
||||
@FileConverter =
|
||||
convert: sinon.stub()
|
||||
thumbnail: sinon.stub()
|
||||
preview: sinon.stub()
|
||||
@keyBuilder =
|
||||
addCachingToKey: sinon.stub()
|
||||
getConvertedFolderKey: sinon.stub()
|
||||
@ImageOptimiser =
|
||||
compressPng: sinon.stub()
|
||||
@handler = SandboxedModule.require modulePath, requires:
|
||||
"settings-sharelatex": @settings
|
||||
"./s3Wrapper":@s3Wrapper
|
||||
"./LocalFileWriter":@LocalFileWriter
|
||||
"./FileConverter":@FileConverter
|
||||
"./KeyBuilder": @keyBuilder
|
||||
"./ImageOptimiser":@ImageOptimiser
|
||||
"logger-sharelatex":
|
||||
log:->
|
||||
err:->
|
||||
@bucket = "my_bucket"
|
||||
@key = "key/here"
|
||||
@stubbedPath = "/var/somewhere/path"
|
||||
@format = "png"
|
||||
@formattedStubbedPath = "#{@stubbedPath}.#{@format}"
|
||||
|
||||
describe "insertFile", ->
|
||||
beforeEach ->
|
||||
@stream = {}
|
||||
@s3Wrapper.deleteDirectory.callsArgWith(2)
|
||||
@s3Wrapper.sendStreamToS3.callsArgWith(3)
|
||||
|
||||
it "should send file to s3", (done)->
|
||||
@handler.insertFile @bucket, @key, @stream, =>
|
||||
@s3Wrapper.sendStreamToS3.calledWith(@bucket, @key, @stream).should.equal true
|
||||
done()
|
||||
|
||||
it "should delete the convetedKey folder", (done)->
|
||||
@keyBuilder.getConvertedFolderKey.returns(@stubbedConvetedKey)
|
||||
@handler.insertFile @bucket, @key, @stream, =>
|
||||
@s3Wrapper.deleteDirectory.calledWith(@bucket, @stubbedConvetedKey).should.equal true
|
||||
done()
|
||||
|
||||
describe "deleteFile", ->
|
||||
beforeEach ->
|
||||
@keyBuilder.getConvertedFolderKey.returns(@stubbedConvetedKey)
|
||||
@s3Wrapper.deleteFile.callsArgWith(2)
|
||||
|
||||
it "should tell the s3 wrapper to delete the file", (done)->
|
||||
@handler.deleteFile @bucket, @key, =>
|
||||
@s3Wrapper.deleteFile.calledWith(@bucket, @key).should.equal true
|
||||
done()
|
||||
|
||||
it "should tell the s3 wrapper to delete the cached foler", (done)->
|
||||
@handler.deleteFile @bucket, @key, =>
|
||||
@s3Wrapper.deleteFile.calledWith(@bucket, @stubbedConvetedKey).should.equal true
|
||||
done()
|
||||
|
||||
describe "getFile", ->
|
||||
beforeEach ->
|
||||
@handler._getStandardFile = sinon.stub().callsArgWith(3)
|
||||
@handler._getConvertedFile = sinon.stub().callsArgWith(3)
|
||||
|
||||
it "should call _getStandardFile if no format or style are defined", (done)->
|
||||
|
||||
@handler.getFile @bucket, @key, null, =>
|
||||
@handler._getStandardFile.called.should.equal true
|
||||
@handler._getConvertedFile.called.should.equal false
|
||||
done()
|
||||
|
||||
it "should call _getConvertedFile if a format is defined", (done)->
|
||||
@handler.getFile @bucket, @key, format:"png", =>
|
||||
@handler._getStandardFile.called.should.equal false
|
||||
@handler._getConvertedFile.called.should.equal true
|
||||
done()
|
||||
|
||||
|
||||
describe "_getStandardFile", ->
|
||||
|
||||
beforeEach ->
|
||||
@fileStream = {on:->}
|
||||
@s3Wrapper.getFileStream.callsArgWith(2, "err", @fileStream)
|
||||
|
||||
it "should get the stream from s3 ", (done)->
|
||||
@handler.getFile @bucket, @key, null, =>
|
||||
@s3Wrapper.getFileStream.calledWith(@bucket, @key).should.equal true
|
||||
done()
|
||||
|
||||
it "should return the stream and error", (done)->
|
||||
@handler.getFile @bucket, @key, null, (err, stream)=>
|
||||
err.should.equal "err"
|
||||
stream.should.equal @fileStream
|
||||
done()
|
||||
|
||||
describe "_getConvertedFile", ->
|
||||
|
||||
it "should getFileStream if it does exists", (done)->
|
||||
@s3Wrapper.checkIfFileExists.callsArgWith(2, null, true)
|
||||
@s3Wrapper.getFileStream.callsArgWith(2)
|
||||
@handler._getConvertedFile @bucket, @key, {}, =>
|
||||
@s3Wrapper.getFileStream.calledWith(@bucket).should.equal true
|
||||
done()
|
||||
|
||||
it "should call _getConvertedFileAndCache if it does exists", (done)->
|
||||
@s3Wrapper.checkIfFileExists.callsArgWith(2, null, false)
|
||||
@handler._getConvertedFileAndCache = sinon.stub().callsArgWith(4)
|
||||
@handler._getConvertedFile @bucket, @key, {}, =>
|
||||
@handler._getConvertedFileAndCache.calledWith(@bucket, @key).should.equal true
|
||||
done()
|
||||
|
||||
describe "_getConvertedFileAndCache", ->
|
||||
|
||||
it "should _convertFile ", (done)->
|
||||
@s3Wrapper.sendFileToS3 = sinon.stub().callsArgWith(3)
|
||||
@s3Wrapper.getFileStream = sinon.stub().callsArgWith(2)
|
||||
@convetedKey = @key+"converted"
|
||||
@handler._convertFile = sinon.stub().callsArgWith(3, null, @stubbedPath)
|
||||
@ImageOptimiser.compressPng = sinon.stub().callsArgWith(1)
|
||||
@handler._getConvertedFileAndCache @bucket, @key, @convetedKey, {}, =>
|
||||
@handler._convertFile.called.should.equal true
|
||||
@s3Wrapper.sendFileToS3.calledWith(@bucket, @convetedKey, @stubbedPath).should.equal true
|
||||
@s3Wrapper.getFileStream.calledWith(@bucket, @convetedKey).should.equal true
|
||||
@ImageOptimiser.compressPng.calledWith(@stubbedPath).should.equal true
|
||||
done()
|
||||
|
||||
describe "_convertFile", ->
|
||||
beforeEach ->
|
||||
@FileConverter.convert.callsArgWith(2, null, @formattedStubbedPath)
|
||||
@FileConverter.thumbnail.callsArgWith(1, null, @formattedStubbedPath)
|
||||
@FileConverter.preview.callsArgWith(1, null, @formattedStubbedPath)
|
||||
@handler._writeS3FileToDisk = sinon.stub().callsArgWith(2, null, @stubbedPath)
|
||||
|
||||
it "should call thumbnail on the writer path if style was thumbnail was specified", (done)->
|
||||
@handler._convertFile @bucket, @key, style:"thumbnail", (err, path)=>
|
||||
path.should.equal @formattedStubbedPath
|
||||
@FileConverter.thumbnail.calledWith(@stubbedPath).should.equal true
|
||||
done()
|
||||
|
||||
it "should call preview on the writer path if style was preview was specified", (done)->
|
||||
@handler._convertFile @bucket, @key, style:"preview", (err, path)=>
|
||||
path.should.equal @formattedStubbedPath
|
||||
@FileConverter.preview.calledWith(@stubbedPath).should.equal true
|
||||
done()
|
||||
|
||||
it "should call convert on the writer path if a format was specified", (done)->
|
||||
@handler._convertFile @bucket, @key, format:@format, (err, path)=>
|
||||
path.should.equal @formattedStubbedPath
|
||||
@FileConverter.convert.calledWith(@stubbedPath, @format).should.equal true
|
||||
done()
|
||||
|
||||
|
||||
|
||||
|
|
@ -0,0 +1,60 @@
|
|||
assert = require("chai").assert
|
||||
sinon = require('sinon')
|
||||
chai = require('chai')
|
||||
should = chai.should()
|
||||
expect = chai.expect
|
||||
modulePath = "../../../app/js/ImageOptimiser.js"
|
||||
SandboxedModule = require('sandboxed-module')
|
||||
|
||||
describe "ImageOptimiser", ->
|
||||
|
||||
beforeEach ->
|
||||
|
||||
@fs =
|
||||
createReadStream:sinon.stub()
|
||||
createWriteStream:sinon.stub()
|
||||
rename:sinon.stub()
|
||||
@pngcrush = class PngCrush
|
||||
pipe:->
|
||||
on: ->
|
||||
|
||||
@optimiser = SandboxedModule.require modulePath, requires:
|
||||
"fs":@fs
|
||||
"pngcrush":@pngcrush
|
||||
"logger-sharelatex":
|
||||
log:->
|
||||
err:->
|
||||
|
||||
@sourcePath = "/this/path/here.eps"
|
||||
@writeStream =
|
||||
pipe:->
|
||||
on: (type, cb)->
|
||||
if type == "finish"
|
||||
cb()
|
||||
@sourceStream =
|
||||
pipe:->
|
||||
return pipe:->
|
||||
on:->
|
||||
@error = "Error"
|
||||
|
||||
describe "compressPng", ->
|
||||
|
||||
beforeEach ->
|
||||
@fs.createReadStream.returns(@sourceStream)
|
||||
@fs.createWriteStream.returns(@writeStream)
|
||||
@fs.rename.callsArgWith(2)
|
||||
|
||||
it "should get the file stream", (done)->
|
||||
@optimiser.compressPng @sourcePath, (err)=>
|
||||
@fs.createReadStream.calledWith(@sourcePath).should.equal true
|
||||
done()
|
||||
|
||||
it "should create a compressed file stream", (done)->
|
||||
@optimiser.compressPng @sourcePath, (err)=>
|
||||
@fs.createWriteStream.calledWith("#{@sourcePath}-optimised")
|
||||
done()
|
||||
|
||||
it "should rename the file after completion", (done)->
|
||||
@optimiser.compressPng @sourcePath, (err)=>
|
||||
@fs.rename.calledWith("#{@sourcePath}-optimised", @sourcePath).should.equal true
|
||||
done()
|
39
services/filestore/test/unit/coffee/KeybuilderTests.coffee
Normal file
39
services/filestore/test/unit/coffee/KeybuilderTests.coffee
Normal file
|
@ -0,0 +1,39 @@
|
|||
|
||||
assert = require("chai").assert
|
||||
sinon = require('sinon')
|
||||
chai = require('chai')
|
||||
should = chai.should()
|
||||
expect = chai.expect
|
||||
modulePath = "../../../app/js/KeyBuilder.js"
|
||||
SandboxedModule = require('sandboxed-module')
|
||||
|
||||
describe "LocalFileWriter", ->
|
||||
|
||||
beforeEach ->
|
||||
|
||||
@keyBuilder = SandboxedModule.require modulePath, requires:
|
||||
"logger-sharelatex":
|
||||
log:->
|
||||
err:->
|
||||
@key = "123/456"
|
||||
|
||||
describe "cachedKey", ->
|
||||
|
||||
it "should add the fomat on", ->
|
||||
opts =
|
||||
format: "png"
|
||||
newKey = @keyBuilder.addCachingToKey @key, opts
|
||||
newKey.should.equal "#{@key}-converted-cache/format-png"
|
||||
|
||||
it "should add the style on", ->
|
||||
opts =
|
||||
style: "thumbnail"
|
||||
newKey = @keyBuilder.addCachingToKey @key, opts
|
||||
newKey.should.equal "#{@key}-converted-cache/style-thumbnail"
|
||||
|
||||
it "should add format on first", ->
|
||||
opts =
|
||||
style: "thumbnail"
|
||||
format: "png"
|
||||
newKey = @keyBuilder.addCachingToKey @key, opts
|
||||
newKey.should.equal "#{@key}-converted-cache/format-png-style-thumbnail"
|
|
@ -0,0 +1,59 @@
|
|||
|
||||
assert = require("chai").assert
|
||||
sinon = require('sinon')
|
||||
chai = require('chai')
|
||||
should = chai.should()
|
||||
expect = chai.expect
|
||||
modulePath = "../../../app/js/LocalFileWriter.js"
|
||||
SandboxedModule = require('sandboxed-module')
|
||||
|
||||
describe "LocalFileWriter", ->
|
||||
|
||||
beforeEach ->
|
||||
|
||||
@writeStream =
|
||||
on: (type, cb)->
|
||||
if type == "finish"
|
||||
cb()
|
||||
@fs =
|
||||
createWriteStream : sinon.stub().returns(@writeStream)
|
||||
unlink: sinon.stub()
|
||||
@writer = SandboxedModule.require modulePath, requires:
|
||||
"fs": @fs
|
||||
"logger-sharelatex":
|
||||
log:->
|
||||
err:->
|
||||
@stubbedFsPath = "something/uploads/eio2k1j3"
|
||||
|
||||
describe "writeStrem", ->
|
||||
beforeEach ->
|
||||
@writer._getPath = sinon.stub().returns(@stubbedFsPath)
|
||||
|
||||
it "write the stream to ./uploads", (done)->
|
||||
stream =
|
||||
pipe: (dest)=>
|
||||
dest.should.equal @writeStream
|
||||
done()
|
||||
on: ->
|
||||
@writer.writeStream stream, null, ()=>
|
||||
|
||||
it "should send the path in the callback", (done)->
|
||||
stream =
|
||||
pipe: (dest)=>
|
||||
on: (type, cb)->
|
||||
if type == "end"
|
||||
cb()
|
||||
@writer.writeStream stream, null, (err, fsPath)=>
|
||||
fsPath.should.equal @stubbedFsPath
|
||||
done()
|
||||
|
||||
describe "delete file", ->
|
||||
|
||||
it "should unlink the file", (done)->
|
||||
error = "my error"
|
||||
@fs.unlink.callsArgWith(1, error)
|
||||
@writer.deleteFile @stubbedFsPath, (err)=>
|
||||
@fs.unlink.calledWith(@stubbedFsPath).should.equal true
|
||||
err.should.equal error
|
||||
done()
|
||||
|
193
services/filestore/test/unit/coffee/s3WrapperTests.coffee
Normal file
193
services/filestore/test/unit/coffee/s3WrapperTests.coffee
Normal file
|
@ -0,0 +1,193 @@
|
|||
assert = require("chai").assert
|
||||
sinon = require('sinon')
|
||||
chai = require('chai')
|
||||
should = chai.should()
|
||||
expect = chai.expect
|
||||
modulePath = "../../../app/js/s3Wrapper.js"
|
||||
SandboxedModule = require('sandboxed-module')
|
||||
|
||||
describe "s3WrapperTests", ->
|
||||
|
||||
beforeEach ->
|
||||
@settings =
|
||||
s3:
|
||||
secret: "secret"
|
||||
key: "this_key"
|
||||
buckets:
|
||||
user_files:"sl_user_files"
|
||||
@stubbedKnoxClient =
|
||||
putFile:sinon.stub()
|
||||
copyFile:sinon.stub()
|
||||
list: sinon.stub()
|
||||
deleteMultiple: sinon.stub()
|
||||
@knox =
|
||||
createClient: sinon.stub().returns(@stubbedKnoxClient)
|
||||
@LocalFileWriter =
|
||||
writeStream: sinon.stub()
|
||||
deleteFile: sinon.stub()
|
||||
@requires =
|
||||
"knox": @knox
|
||||
"settings-sharelatex": @settings
|
||||
"./LocalFileWriter":@LocalFileWriter
|
||||
"logger-sharelatex":
|
||||
log:->
|
||||
err:->
|
||||
@key = "my/key"
|
||||
@bucketName = "my-bucket"
|
||||
@error = "my errror"
|
||||
|
||||
describe "Pipe to dest", ->
|
||||
|
||||
it "should use correct options", (done)->
|
||||
|
||||
stubbedReadStream = {on:->}
|
||||
dest = {my:"object"}
|
||||
@request = (opts)=>
|
||||
return stubbedReadStream
|
||||
@requires["request"] = @request
|
||||
@s3Wrapper = SandboxedModule.require modulePath, requires: @requires
|
||||
@s3Wrapper.getFileStream @bucketName, @key, (err, readStream)->
|
||||
readStream.should.equal stubbedReadStream
|
||||
done()
|
||||
|
||||
describe "sendFileToS3", ->
|
||||
|
||||
beforeEach ->
|
||||
@s3Wrapper = SandboxedModule.require modulePath, requires: @requires
|
||||
@stubbedKnoxClient.putFile.returns on:->
|
||||
|
||||
it "should put file with knox", (done)->
|
||||
@LocalFileWriter.deleteFile.callsArgWith(1)
|
||||
@stubbedKnoxClient.putFile.callsArgWith(2, @error)
|
||||
@s3Wrapper.sendFileToS3 @bucketName, @key, @fsPath, (err)=>
|
||||
@stubbedKnoxClient.putFile.calledWith(@fsPath, @key).should.equal true
|
||||
err.should.equal @error
|
||||
done()
|
||||
|
||||
it "should delete the file and pass the error with it", (done)->
|
||||
@LocalFileWriter.deleteFile.callsArgWith(1)
|
||||
@stubbedKnoxClient.putFile.callsArgWith(2, @error)
|
||||
@s3Wrapper.sendFileToS3 @bucketName, @key, @fsPath, (err)=>
|
||||
@stubbedKnoxClient.putFile.calledWith(@fsPath, @key).should.equal true
|
||||
err.should.equal @error
|
||||
done()
|
||||
|
||||
describe "sendStreamToS3", ->
|
||||
beforeEach ->
|
||||
@fsPath = "to/some/where"
|
||||
@origin =
|
||||
on:->
|
||||
@s3Wrapper = SandboxedModule.require modulePath, requires: @requires
|
||||
@s3Wrapper.sendFileToS3 = sinon.stub().callsArgWith(3)
|
||||
|
||||
it "should send stream to LocalFileWriter", (done)->
|
||||
@LocalFileWriter.deleteFile.callsArgWith(1)
|
||||
@LocalFileWriter.writeStream.callsArgWith(2, null, @fsPath)
|
||||
@s3Wrapper.sendStreamToS3 @bucketName, @key, @origin, =>
|
||||
@LocalFileWriter.writeStream.calledWith(@origin).should.equal true
|
||||
done()
|
||||
|
||||
it "should return the error from LocalFileWriter", (done)->
|
||||
@LocalFileWriter.deleteFile.callsArgWith(1)
|
||||
@LocalFileWriter.writeStream.callsArgWith(2, @error)
|
||||
@s3Wrapper.sendStreamToS3 @bucketName, @key, @origin, (err)=>
|
||||
err.should.equal @error
|
||||
done()
|
||||
|
||||
it "should send the file to s3", (done)->
|
||||
@LocalFileWriter.deleteFile.callsArgWith(1)
|
||||
@LocalFileWriter.writeStream.callsArgWith(2)
|
||||
@s3Wrapper.sendStreamToS3 @bucketName, @key, @origin, (err)=>
|
||||
@s3Wrapper.sendFileToS3.called.should.equal true
|
||||
done()
|
||||
|
||||
describe "copyFile", ->
|
||||
beforeEach ->
|
||||
@sourceKey = "my/key"
|
||||
@destKey = "my/dest/key"
|
||||
@s3Wrapper = SandboxedModule.require modulePath, requires: @requires
|
||||
|
||||
it "should use knox to copy file", (done)->
|
||||
@stubbedKnoxClient.copyFile.callsArgWith(2, @error)
|
||||
@s3Wrapper.copyFile @bucketName, @sourceKey, @destKey, (err)=>
|
||||
err.should.equal @error
|
||||
@stubbedKnoxClient.copyFile.calledWith(@sourceKey, @destKey).should.equal true
|
||||
done()
|
||||
|
||||
describe "deleteDirectory", ->
|
||||
|
||||
beforeEach ->
|
||||
@s3Wrapper = SandboxedModule.require modulePath, requires: @requires
|
||||
|
||||
it "should list the contents passing them onto multi delete", (done)->
|
||||
data =
|
||||
Contents: [{Key:"1234"}, {Key: "456"}]
|
||||
@stubbedKnoxClient.list.callsArgWith(1, null, data)
|
||||
@stubbedKnoxClient.deleteMultiple.callsArgWith(1)
|
||||
@s3Wrapper.deleteDirectory @bucketName, @key, (err)=>
|
||||
@stubbedKnoxClient.deleteMultiple.calledWith(["1234","456"]).should.equal true
|
||||
done()
|
||||
|
||||
describe "deleteFile", ->
|
||||
|
||||
it "should use correct options", (done)->
|
||||
@request = sinon.stub().callsArgWith(1)
|
||||
@requires["request"] = @request
|
||||
@s3Wrapper = SandboxedModule.require modulePath, requires: @requires
|
||||
|
||||
@s3Wrapper.deleteFile @bucketName, @key, (err)=>
|
||||
opts = @request.args[0][0]
|
||||
assert.deepEqual(opts.aws, {key:@settings.s3.key, secret:@settings.s3.secret, bucket:@bucketName})
|
||||
opts.method.should.equal "delete"
|
||||
opts.timeout.should.equal (30*1000)
|
||||
opts.uri.should.equal "https://#{@bucketName}.s3.amazonaws.com/#{@key}"
|
||||
done()
|
||||
|
||||
it "should return the error", (done)->
|
||||
@request = sinon.stub().callsArgWith(1, @error)
|
||||
@requires["request"] = @request
|
||||
@s3Wrapper = SandboxedModule.require modulePath, requires: @requires
|
||||
|
||||
@s3Wrapper.deleteFile @bucketName, @key, (err)=>
|
||||
err.should.equal @error
|
||||
done()
|
||||
|
||||
describe "checkIfFileExists", ->
|
||||
|
||||
it "should use correct options", (done)->
|
||||
@request = sinon.stub().callsArgWith(1, null, statusCode:200)
|
||||
@requires["request"] = @request
|
||||
@s3Wrapper = SandboxedModule.require modulePath, requires: @requires
|
||||
|
||||
@s3Wrapper.checkIfFileExists @bucketName, @key, (err)=>
|
||||
opts = @request.args[0][0]
|
||||
assert.deepEqual(opts.aws, {key:@settings.s3.key, secret:@settings.s3.secret, bucket:@bucketName})
|
||||
opts.method.should.equal "head"
|
||||
opts.timeout.should.equal (30*1000)
|
||||
opts.uri.should.equal "https://#{@bucketName}.s3.amazonaws.com/#{@key}"
|
||||
done()
|
||||
|
||||
it "should return true for a 200", (done)->
|
||||
@request = sinon.stub().callsArgWith(1, null, statusCode:200)
|
||||
@requires["request"] = @request
|
||||
@s3Wrapper = SandboxedModule.require modulePath, requires: @requires
|
||||
@s3Wrapper.checkIfFileExists @bucketName, @key, (err, exists)=>
|
||||
exists.should.equal true
|
||||
done()
|
||||
|
||||
it "should return false for a non 200", (done)->
|
||||
@request = sinon.stub().callsArgWith(1, null, statusCode:404)
|
||||
@requires["request"] = @request
|
||||
@s3Wrapper = SandboxedModule.require modulePath, requires: @requires
|
||||
@s3Wrapper.checkIfFileExists @bucketName, @key, (err, exists)=>
|
||||
exists.should.equal false
|
||||
done()
|
||||
|
||||
it "should return the error", (done)->
|
||||
@request = sinon.stub().callsArgWith(1, @error, {})
|
||||
@requires["request"] = @request
|
||||
@s3Wrapper = SandboxedModule.require modulePath, requires: @requires
|
||||
|
||||
@s3Wrapper.checkIfFileExists @bucketName, @key, (err)=>
|
||||
err.should.equal @error
|
||||
done()
|
0
services/filestore/uploads/.gitignore
vendored
Normal file
0
services/filestore/uploads/.gitignore
vendored
Normal file
Loading…
Reference in a new issue