mirror of
https://github.com/codeninjasllc/codecombat.git
synced 2024-11-25 00:28:31 -05:00
209 lines
7 KiB
CoffeeScript
209 lines
7 KiB
CoffeeScript
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() or user.isArtisan()
|
|
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}"
|