From 5882ce7f2541e088a43bda01e9b5d2ddaf90db62 Mon Sep 17 00:00:00 2001 From: "Dmitriy A. Golev" Date: Fri, 24 Jul 2015 14:40:42 +0300 Subject: [PATCH 1/2] Server's `onBeforeUpload `, `dataToSchema ` methods --- README.md | 13 +++-- files.coffee | 145 +++++++++++++++++++++++---------------------------- 2 files changed, 76 insertions(+), 82 deletions(-) diff --git a/README.md b/README.md index 0c724bca..e8f2cf87 100644 --- a/README.md +++ b/README.md @@ -102,6 +102,13 @@ API * __Note:__ Collection can not be `public` and `protected` at the same time! * __Note:__ `integrityCheck` is __not__ guaranteed! * __Note:__ `play` and force `download` features is __not__ guaranteed! + - `onBeforeUpload` {*Function*} - Callback triggered right before upload on __client__ and right after recieving a chunk on __server__, with __no arguments__: + * Context of the function is {*Object*}, with: + - `extension`, alias `ext` + - `mime-type`, alias `type` + - `size` + * __return__ `true` to continue + * __return__ `false` to abort or {*String*} to abort upload with message - `onbeforeunloadMessage` {*String* or *Function*} - Message shown to user when closing browser's window or tab, while upload in the progress - `allowClientCode` {*Boolean*} - Allow to run `remove()` from client - `debug` {*Boolean*} - Turn on/of debugging and extra logging @@ -345,7 +352,7 @@ Methods - `onBeforeUpload` {*Function*} - Callback triggered right before upload is started, with __no arguments__: * Context of the function is `File` - so you are able to check for extension, mime-type, size and etc. * __return__ `true` to continue - * __return__ `false` to abort upload + * __return__ `false` to abort or {*String*} to abort upload with message - `streams` {*Number*} - Quantity of parallel upload streams Returns {*Object*}, with properties: @@ -485,7 +492,7 @@ uploads.write buffer , (err, fileObj) -> # Download File - window.open uploads.link(fileObj, 'original'), '_parent' + window.open uploads.link(fileObj, 'original')+'?download=true', '_parent' ``` ###### `load(url, options, callback)` [*Server*] @@ -509,5 +516,5 @@ uploads.load 'http://domain.com/small.png' , (err, fileObj) -> # Download File - window.open uploads.link(fileObj, 'original'), '_parent' + window.open uploads.link(fileObj, 'original')+'?download=true', '_parent' ``` \ No newline at end of file diff --git a/files.coffee b/files.coffee index d5967a84..51fac64e 100755 --- a/files.coffee +++ b/files.coffee @@ -88,6 +88,7 @@ class Meteor.Files @protected = false if not @protected @public = false if not @public @strict = true if not @strict + @onBeforeUpload = false if not @onBeforeUpload @onbeforeunloadMessage = 'Upload in a progress... Do you want to abort?' if not @onbeforeunloadMessage if @protected and Meteor.isClient @@ -168,7 +169,6 @@ class Meteor.Files @search = {} @cacheControl = 'public, max-age=31536000' - @collection.attachSchema @schema @collection.deny @@ -273,10 +273,17 @@ class Meteor.Files check partsQty, Number check fileSize, Number - @unblock() console.info "Meteor.Files Debugger: [MeteorFileWrite] {name: #{randFileName}, meta:#{meta}}" if self.debug console.info "Meteor.Files Debugger: Received chunk ##{currentChunk} of #{chunksQty} chunks, in part: #{part}, file: #{fileData.name or fileData.fileName}" if self.debug + if @onBeforeUpload and _.isFunction @onBeforeUpload + isUploadAllowed = @onBeforeUpload.call fileData + if isUploadAllowed isnt true + end new Meteor.Error(403, if _.isString(isUploadAllowed) then isUploadAllowed else "@onBeforeUpload() returned false"), null + return false + + @unblock() + i = 0 binary = '' while i < unitArray.byteLength @@ -289,35 +296,20 @@ class Meteor.Files fileName = cleanName(fileData.name or fileData.fileName) ext = fileName.split('.').pop() - _newId = String.rand 32, 'ABCDEFabcdef' if self.public - pathName = if self.public then "#{self.storagePath}/original-#{_newId}" else "#{self.storagePath}/#{randFileName}" - path = if self.public then "#{self.storagePath}/original-#{_newId}.#{ext}" else "#{self.storagePath}/#{randFileName}.#{ext}" + pathName = if self.public then "#{self.storagePath}/original-#{randFileName}" else "#{self.storagePath}/#{randFileName}" + path = if self.public then "#{self.storagePath}/original-#{randFileName}.#{ext}" else "#{self.storagePath}/#{randFileName}.#{ext}" pathPart = if partsQty > 1 then "#{pathName}_#{part}.#{ext}" else path - result = + result = self.dataToSchema name: fileName extension: ext path: path meta: meta type: fileData.type size: fileData.size - chunk: currentChunk - versions: - original: - path: path - size: fileData.size - type: fileData.type - extension: ext - last: last - isVideo: fileData.type.toLowerCase().indexOf("video") > -1 - isAudio: fileData.type.toLowerCase().indexOf("audio") > -1 - isImage: fileData.type.toLowerCase().indexOf("image") > -1 - _prefix: self._prefix - _collectionName: self.collectionName - _storagePath: self.storagePath - _downloadRoute: self.downloadRoute - - result._id = _newId if self.public + + result.chunk = currentChunk + result.last = last if first fs.outputFileSync pathPart, binary, 'binary' @@ -337,6 +329,7 @@ class Meteor.Files fs.renameSync pathName + '_1.' + ext, path fs.chmod path, self.permissions + result._id = randFileName if self.public result._id = self.collection.insert _.clone result console.info "Meteor.Files Debugger: The file #{fileName} (binary) was saved to #{path}" if self.debug @@ -344,6 +337,29 @@ class Meteor.Files Meteor.methods _methods + dataToSchema: (data) -> + return { + name: data.name + extension: data.extension + path: data.path + meta: data.meta + type: data.type + size: data.size + versions: + original: + path: data.path + size: data.size + type: data.type + extension: data.extension + isVideo: data.type.toLowerCase().indexOf("video") > -1 + isAudio: data.type.toLowerCase().indexOf("audio") > -1 + isImage: data.type.toLowerCase().indexOf("image") > -1 + _prefix: data._prefix or @_prefix + _collectionName: data._collectionName or @collectionName + _storagePath: data._storagePath or @storagePath + _downloadRoute: data._downloadRoute or @downloadRoute + } + srch: (search) -> if search and _.isString search @search = @@ -370,35 +386,22 @@ class Meteor.Files check callback, Match.Optional Function if @checkAccess() - randFileName = @namingFunction.call null, true + randFileName = if @public then String.rand 32, 'ABCDEFabcdef' else @namingFunction.call null, true fileName = if opts.name or opts.fileName then opts.name or opts.fileName else randFileName extension = fileName.split('.').pop() - path = "#{@storagePath}/#{randFileName}.#{extension}" + path = if @public then "#{@storagePath}/original-#{randFileName}.#{ext}" else "#{@storagePath}/#{randFileName}.#{ext}" opts.type = 'application/*' if not opts.type opts.meta = {} if not opts.meta opts.size = buffer.length if not opts.size - result = + result = @dataToSchema name: fileName extension: extension path: path meta: opts.meta type: opts.type size: opts.size - isVideo: if opts.type then opts.type.toLowerCase().indexOf("video") > -1 else false - isAudio: if opts.type then opts.type.toLowerCase().indexOf("audio") > -1 else false - isImage: if opts.type then opts.type.toLowerCase().indexOf("image") > -1 else false - versions: - original: - path: path - type: opts.type - size: opts.size - extension: extension - _prefix: @_prefix - _collectionName: @collectionName - _storagePath: @storagePath - _downloadRoute: @downloadRoute console.info "Meteor.Files Debugger: The file #{fileName} (binary) was added to #{@collectionName}" if @debug @@ -429,36 +432,23 @@ class Meteor.Files self = @ if @checkAccess() - randFileName = @namingFunction.call null, true + randFileName = if @public then String.rand 32, 'ABCDEFabcdef' else @namingFunction.call null, true fileName = if opts.name or opts.fileName then opts.name or opts.fileName else randFileName extension = fileName.split('.').pop() - path = "#{@storagePath}/#{randFileName}.#{extension}" + path = if @public then "#{@storagePath}/original-#{randFileName}.#{ext}" else "#{@storagePath}/#{randFileName}.#{ext}" opts.meta = {} if not opts.meta request.get(url).on('error', (error)-> throw new Meteor.Error 500, "Error on [load(#{url}, #{opts})]; Error:" + JSON.stringify error ).on('response', (response) -> bound -> - result = + result = self.dataToSchema name: fileName extension: extension path: path meta: opts.meta type: response.headers['content-type'] size: response.headers['content-length'] - isVideo: response.headers['content-type'].toLowerCase().indexOf("video") > -1 - isAudio: response.headers['content-type'].toLowerCase().indexOf("audio") > -1 - isImage: response.headers['content-type'].toLowerCase().indexOf("image") > -1 - versions: - original: - path: path - type: response.headers['content-type'] - size: response.headers['content-length'] - extension: extension - _prefix: self._prefix - _collectionName: self.collectionName - _storagePath: self.storagePath - _downloadRoute: self.downloadRoute console.info "Meteor.Files Debugger: The file #{fileName} (binary) was loaded to #{@collectionName}" if @debug @@ -480,7 +470,7 @@ class Meteor.Files @description Add file from FS to Meteor.Files @returns {Files} - Return this ### - addFile: if Meteor.isServer then (path, opts, callback) -> + addFile: if Meteor.isServer then (path, opts = {}, callback) -> console.info "[addFile(#{path})]" if @debug throw new Meteor.Error 403, "Can not run [addFile()] on public collection" if @public @@ -501,26 +491,14 @@ class Meteor.Files opts.meta = {} if not opts.meta opts.size = fileSize if not opts.size - result = - name: fileName - extension: ext - path: path - meta: opts.meta - type: opts.type - size: opts.size - isVideo: opts.type.toLowerCase().indexOf("video") > -1 - isAudio: opts.type.toLowerCase().indexOf("audio") > -1 - isImage: opts.type.toLowerCase().indexOf("image") > -1 - versions: - original: - path: path - type: opts.type - size: opts.size - extension: ext - _prefix: @_prefix - _collectionName: @collectionName - _storagePath: path.replace "/#{fileName}", '' - _downloadRoute: @downloadRoute + result = @dataToSchema + name: fileName + extension: ext + path: path + meta: opts.meta + type: opts.type + size: opts.size + _storagePath: path.replace "/#{fileName}", '' result._id = @collection.insert _.clone result console.info "The file #{fileName} (binary) was added to #{@collectionName}" if @debug @@ -671,10 +649,11 @@ class Meteor.Files type: file.type name: file.name ext: file.name.split('.').pop() - extension: file.name.split('.').pop() + extension: file.name.split('.').pop() + 'mime-type': file.type file = _.extend file, fileData - randFileName = @namingFunction.call null, true + randFileName = if @public then String.rand 32, 'ABCDEFabcdef' else @namingFunction.call null, true partSize = Math.ceil file.size / streams parts = [] uploaded = 0 @@ -702,12 +681,18 @@ class Meteor.Files result.progress.set 0 onUploaded and onUploaded.call self, error, data - if onBeforeUpload + if onBeforeUpload and _.isFunction onBeforeUpload isUploadAllowed = onBeforeUpload.call file if isUploadAllowed isnt true end new Meteor.Error(403, if _.isString(isUploadAllowed) then isUploadAllowed else "onBeforeUpload() returned false"), null return false + if @onBeforeUpload and _.isFunction @onBeforeUpload + isUploadAllowed = @onBeforeUpload.call file + if isUploadAllowed isnt true + end new Meteor.Error(403, if _.isString(isUploadAllowed) then isUploadAllowed else "@onBeforeUpload() returned false"), null + return false + upload = (filePart, part, chunksQtyInPart, fileReader) -> currentChunk = 1 first = true @@ -758,6 +743,8 @@ class Meteor.Files upload.call null, file.slice(part.from, part.to), i + 1, part.chunksQty, fileReader return result + else + console.warn "Meteor.Files: [insert({file: 'file', ..})]: file property is required" else undefined From f7c74c619d77a0d4154a8b1712e0edb83b97c14b Mon Sep 17 00:00:00 2001 From: "Dmitriy A. Golev" Date: Fri, 24 Jul 2015 16:58:21 +0300 Subject: [PATCH 2/2] v1.3.4 - Fix minor bugs - Add `onBeforeUpload` for server side (see docs) - Faster code --- .versions | 2 +- files.coffee | 29 +++++++++++++++-------------- package.js | 2 +- 3 files changed, 17 insertions(+), 16 deletions(-) diff --git a/.versions b/.versions index c7817205..eb12ff41 100644 --- a/.versions +++ b/.versions @@ -33,7 +33,7 @@ mongo@1.1.0 observe-sequence@1.0.6 ordered-dict@1.0.3 ostrio:cookies@1.0.1 -ostrio:files@1.3.3 +ostrio:files@1.3.4 ostrio:jsextensions@0.0.4 random@1.0.3 reactive-dict@1.1.0 diff --git a/files.coffee b/files.coffee index 51fac64e..15d08355 100755 --- a/files.coffee +++ b/files.coffee @@ -76,7 +76,7 @@ cp = (to, from) -> ### class Meteor.Files constructor: (config) -> - {@storagePath, @collectionName, @downloadRoute, @schema, @chunkSize, @namingFunction, @debug, @onbeforeunloadMessage, @permissions, @allowClientCode, @integrityCheck, @protected, @public, @strict} = config if config + {@storagePath, @collectionName, @downloadRoute, @schema, @chunkSize, @namingFunction, @debug, @onbeforeunloadMessage, @permissions, @allowClientCode, @onBeforeUpload, @integrityCheck, @protected, @public, @strict} = config if config @collectionName = 'MeteorUploadFiles' if not @collectionName @chunkSize = 272144 if not @chunkSize @@ -276,11 +276,10 @@ class Meteor.Files console.info "Meteor.Files Debugger: [MeteorFileWrite] {name: #{randFileName}, meta:#{meta}}" if self.debug console.info "Meteor.Files Debugger: Received chunk ##{currentChunk} of #{chunksQty} chunks, in part: #{part}, file: #{fileData.name or fileData.fileName}" if self.debug - if @onBeforeUpload and _.isFunction @onBeforeUpload - isUploadAllowed = @onBeforeUpload.call fileData + if self.onBeforeUpload and _.isFunction self.onBeforeUpload + isUploadAllowed = self.onBeforeUpload.call fileData if isUploadAllowed isnt true - end new Meteor.Error(403, if _.isString(isUploadAllowed) then isUploadAllowed else "@onBeforeUpload() returned false"), null - return false + throw new Meteor.Error(403, if _.isString(isUploadAllowed) then isUploadAllowed else "@onBeforeUpload() returned false") @unblock() @@ -295,14 +294,14 @@ class Meteor.Files str.replace(/\.\./g, '').replace /\//g, '' fileName = cleanName(fileData.name or fileData.fileName) - ext = fileName.split('.').pop() + extension = fileName.split('.').pop() pathName = if self.public then "#{self.storagePath}/original-#{randFileName}" else "#{self.storagePath}/#{randFileName}" - path = if self.public then "#{self.storagePath}/original-#{randFileName}.#{ext}" else "#{self.storagePath}/#{randFileName}.#{ext}" - pathPart = if partsQty > 1 then "#{pathName}_#{part}.#{ext}" else path + path = if self.public then "#{self.storagePath}/original-#{randFileName}.#{extension}" else "#{self.storagePath}/#{randFileName}.#{extension}" + pathPart = if partsQty > 1 then "#{pathName}_#{part}.#{extension}" else path result = self.dataToSchema name: fileName - extension: ext + extension: extension path: path meta: meta type: fileData.type @@ -323,10 +322,10 @@ class Meteor.Files buffers = [] i = 2 while i <= partsQty - fs.appendFileSync pathName + '_1.' + ext, fs.readFileSync(pathName + '_' + i + '.' + ext), 'binary' - fs.unlink pathName + '_' + i + '.' + ext + fs.appendFileSync pathName + '_1.' + extension, fs.readFileSync(pathName + '_' + i + '.' + extension), 'binary' + fs.unlink pathName + '_' + i + '.' + extension i++ - fs.renameSync pathName + '_1.' + ext, path + fs.renameSync pathName + '_1.' + extension, path fs.chmod path, self.permissions result._id = randFileName if self.public @@ -389,7 +388,7 @@ class Meteor.Files randFileName = if @public then String.rand 32, 'ABCDEFabcdef' else @namingFunction.call null, true fileName = if opts.name or opts.fileName then opts.name or opts.fileName else randFileName extension = fileName.split('.').pop() - path = if @public then "#{@storagePath}/original-#{randFileName}.#{ext}" else "#{@storagePath}/#{randFileName}.#{ext}" + path = if @public then "#{@storagePath}/original-#{randFileName}.#{extension}" else "#{@storagePath}/#{randFileName}.#{extension}" opts.type = 'application/*' if not opts.type opts.meta = {} if not opts.meta @@ -435,7 +434,7 @@ class Meteor.Files randFileName = if @public then String.rand 32, 'ABCDEFabcdef' else @namingFunction.call null, true fileName = if opts.name or opts.fileName then opts.name or opts.fileName else randFileName extension = fileName.split('.').pop() - path = if @public then "#{@storagePath}/original-#{randFileName}.#{ext}" else "#{@storagePath}/#{randFileName}.#{ext}" + path = if @public then "#{@storagePath}/original-#{randFileName}.#{extension}" else "#{@storagePath}/#{randFileName}.#{extension}" opts.meta = {} if not opts.meta request.get(url).on('error', (error)-> @@ -711,10 +710,12 @@ class Meteor.Files if chunksQtyInPart is 1 Meteor.call self.methodNames.MeteorFileWrite, unitArray, fileData, meta, first, chunksQtyInPart, currentChunk, totalSentChunks, randFileName, part, streams, file.size, (error, data) -> + return end error if error if data.last end error, data else Meteor.call self.methodNames.MeteorFileWrite, unitArray, fileData, meta, first, chunksQtyInPart, currentChunk, totalSentChunks, randFileName, part, streams, file.size, (error, data)-> + return end error if error if not result.onPause.get() if data.chunk + 1 <= chunksQtyInPart from = currentChunk * self.chunkSize diff --git a/package.js b/package.js index 5a548364..fa2b8f25 100755 --- a/package.js +++ b/package.js @@ -1,6 +1,6 @@ Package.describe({ name: 'ostrio:files', - version: '1.3.3', + version: '1.3.4', summary: 'Upload, Store and Stream (Video & Audio streaming) files to/from file system (FS) via DDP and HTTP', git: 'https://github.com/VeliovGroup/Meteor-Files', documentation: 'README.md'