Grid = require 'gridfs-stream' fs = require 'fs' request = require 'request' mongoose = require 'mongoose' errors = require '../commons/errors' config = require '../../server_config' module.exports.setup = (app) -> app.all '/file*', (req, res) -> return fileGet(req, res) if req.route.method is 'get' return filePost(req, res) if req.route.method is 'post' return fileDelete(req, res) if req.route.method is 'delete' return errors.badMethod(res, ['GET', 'POST', 'DELETE']) fileDelete = (req, res) -> return errors.forbidden(res) unless req.user query = parsePathIntoQuery(req.path) return errors.badInput(res) if not query.filename Grid.gfs.collection('media').findOne query, (err, filedata) => return errors.notFound(res) if not filedata return errors.forbidden(res) unless userCanEditFile(req.user, filedata) Grid.gfs.remove {_id: filedata._id, root: 'media'}, (err) -> return errors.serverError(res) if err return res.end() fileGet = (req, res) -> query = parsePathIntoQuery(req.path) if not query.filename # it's a folder, return folder contents Grid.gfs.collection('media').find query, (err, cursor) -> return errors.serverError(res) if err results = cursor.toArray (err, results) -> return errors.serverError(res) if err res.setHeader('Content-Type', 'text/json') res.send(results) res.end() else # it's a single file Grid.gfs.collection('media').findOne query, (err, filedata) => return errors.notFound(res) if not filedata readstream = Grid.gfs.createReadStream({_id: filedata._id, root: 'media'}) if req.headers['if-modified-since'] is filedata.uploadDate res.status(304) return res.end() res.setHeader('Content-Type', filedata.contentType) res.setHeader('Last-Modified', filedata.uploadDate) res.setHeader('Cache-Control', 'public') readstream.pipe(res) handleStreamEnd(res, res) parsePathIntoQuery = (path) -> path = path[6..] path = decodeURI path try objectId = mongoose.Types.ObjectId(path) query = objectId catch e path = path.split('/') filename = path[path.length-1] path = path[...path.length-1].join('/') query = 'metadata.path': path query.filename = filename if filename query postFileSchema = type: 'object' properties: # source url: { type: 'string', description: 'The url to download the file from.' } postName: { type: 'string', description: 'The input field this file was sent on.' } b64png: { type: 'string', description: 'Raw png data to upload.' } # options force: { type: 'string', 'default': '', description: 'Whether to overwrite existing files (as opposed to throwing an error).' } # metadata filename: { type: 'string', description: 'What the file will be named in the system.' } mimetype: { type: 'string' } name: { type: 'string', description: 'Human readable and searchable string.' } description: { type: 'string' } path: { type: 'string', description: 'What "folder" this file goes into.' } required: ['filename', 'mimetype', 'path'] filePost = (req, res) -> return errors.forbidden(res) unless req.user options = req.body tv4 = require('tv4').tv4 valid = tv4.validate(options, postFileSchema) hasSource = options.url or options.postName or options.b64png # TODO : give tv4.error to badInput return errors.badInput(res) if (not valid) or (not hasSource) return saveURL(req, res) if options.url return saveFile(req, res) if options.postName return savePNG(req, res) if options.b64png saveURL = (req, res) -> options = createPostOptions(req) checkExistence options, req, res, req.body.force, (err) -> return errors.serverError(res) if err writestream = Grid.gfs.createWriteStream(options) request(req.body.url).pipe(writestream) handleStreamEnd(res, writestream) saveFile = (req, res) -> options = createPostOptions(req) checkExistence options, req, res, req.body.force, (err) -> return if err writestream = Grid.gfs.createWriteStream(options) f = req.files[req.body.postName] fileStream = fs.createReadStream(f.path) fileStream.pipe(writestream) handleStreamEnd(res, writestream) savePNG = (req, res) -> options = createPostOptions(req) checkExistence options, req, res, req.body.force, (err) -> return if err writestream = Grid.gfs.createWriteStream(options) img = new Buffer(req.body.b64png, 'base64') streamBuffers = require 'stream-buffers' myReadableStreamBuffer = new streamBuffers.ReadableStreamBuffer({frequency: 10, chunkSize: 2048}) myReadableStreamBuffer.put(img) myReadableStreamBuffer.pipe(writestream) handleStreamEnd(res, writestream) userCanEditFile = (user=null, file=null) -> # no user means 'anyone'. No file means 'any file' return false unless user return true if user.isAdmin() return false unless file return true if file.metadata.creator is user.id return false checkExistence = (options, req, res, force, done) -> q = { filename: options.filename 'metadata.path': options.metadata.path } Grid.gfs.collection('media').find(q).toArray (err, files) -> file = files[0] if file and ((not userCanEditFile(req.user, file) or (not force))) errors.conflict(res, {canForce: userCanEditFile(req.user, file)}) done(true) else if file fullPath = "/file/#{options.metadata.path}/#{options.filename}" clearCloudFlareCacheForFile(fullPath) q = { _id: file._id } q.root = 'media' Grid.gfs.remove q, (err) -> if err errors.serverError(res) return done(true) done() else done() handleStreamEnd = (res, stream) -> stream.on 'close', (f) -> res.send(f) res.end() stream.on 'error', -> return errors.serverError(res) CHUNK_SIZE = 1024*256 createPostOptions = (req) -> unless req.body.name name = req.body.filename.split('.')[0] req.body.name = _.str.humanize(name) path = req.body.path or '' path = path[1...] if path and path[0] is '/' path = path[...path.length-2] if path and path[path.length-1] is '/' options = mode: 'w' filename: req.body.filename chunk_size: CHUNK_SIZE root: 'media' content_type: req.body.mimetype metadata: name: req.body.name path: path creator: ''+req.user._id options.metadata.description = req.body.description if req.body.description? options clearCloudFlareCacheForFile = (path='/file') -> unless config.cloudflare.token console.log 'skipping clearing cloud cache, not configured' return request = require 'request' r = request.post 'https://www.cloudflare.com/api_json.html', (err, httpResponse, body) -> if (err) console.error('CloudFlare file cache clear failed:', body) form = r.form() form.append 'tkn', config.cloudflare.token form.append 'email', 'scott@codecombat.com' form.append 'z', 'codecombat.com' form.append 'a', 'zone_file_purge' form.append 'url', "http://codecombat.com#{path}"