diff --git a/.npm/package/npm-shrinkwrap.json b/.npm/package/npm-shrinkwrap.json index 7a82f7b8..36f0d0b1 100644 --- a/.npm/package/npm-shrinkwrap.json +++ b/.npm/package/npm-shrinkwrap.json @@ -1,29 +1,342 @@ { "dependencies": { - "file-type": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/file-type/-/file-type-4.3.0.tgz", - "from": "file-type@4.3.0" + "ajv": { + "version": "4.11.8", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-4.11.8.tgz", + "from": "ajv@>=4.9.1 <5.0.0" }, - "fs-extra": { + "asn1": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.3.tgz", + "from": "asn1@>=0.2.3 <0.3.0" + }, + "assert-plus": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-0.2.0.tgz", + "from": "assert-plus@>=0.2.0 <0.3.0" + }, + "asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "from": "asynckit@>=0.4.0 <0.5.0" + }, + "aws-sign2": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.6.0.tgz", + "from": "aws-sign2@>=0.6.0 <0.7.0" + }, + "aws4": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.6.0.tgz", + "from": "aws4@>=1.2.1 <2.0.0" + }, + "bcrypt-pbkdf": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.1.tgz", + "from": "bcrypt-pbkdf@>=1.0.0 <2.0.0" + }, + "boom": { + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/boom/-/boom-2.10.1.tgz", + "from": "boom@>=2.0.0 <3.0.0" + }, + "buffer-shims": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/buffer-shims/-/buffer-shims-1.0.0.tgz", + "from": "buffer-shims@>=1.0.0 <1.1.0" + }, + "caseless": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", + "from": "caseless@>=0.12.0 <0.13.0" + }, + "co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "from": "co@>=4.6.0 <5.0.0" + }, + "combined-stream": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.5.tgz", + "from": "combined-stream@>=1.0.5 <1.1.0" + }, + "core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "from": "core-util-is@>=1.0.0 <1.1.0" + }, + "cryptiles": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/cryptiles/-/cryptiles-2.0.5.tgz", + "from": "cryptiles@>=2.0.0 <3.0.0" + }, + "dashdash": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", + "from": "dashdash@>=1.12.0 <2.0.0", + "dependencies": { + "assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "from": "assert-plus@>=1.0.0 <2.0.0" + } + } + }, + "debug": { + "version": "2.6.8", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.8.tgz", + "from": "debug@>=2.0.0 <3.0.0" + }, + "delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "from": "delayed-stream@>=1.0.0 <1.1.0" + }, + "ecc-jsbn": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.1.tgz", + "from": "ecc-jsbn@>=0.1.1 <0.2.0" + }, + "extend": { "version": "3.0.1", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-3.0.1.tgz", - "from": "fs-extra@3.0.1" - }, - "graceful-fs": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.11.tgz", - "from": "graceful-fs@>=4.1.2 <5.0.0" - }, - "jsonfile": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-3.0.0.tgz", - "from": "jsonfile@>=3.0.0 <4.0.0" - }, - "universalify": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.0.tgz", - "from": "universalify@>=0.1.0 <0.2.0" + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.1.tgz", + "from": "extend@>=3.0.0 <3.1.0" + }, + "extsprintf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.0.2.tgz", + "from": "extsprintf@1.0.2" + }, + "forever-agent": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", + "from": "forever-agent@>=0.6.1 <0.7.0" + }, + "form-data": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.1.4.tgz", + "from": "form-data@>=2.1.1 <2.2.0" + }, + "getpass": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", + "from": "getpass@>=0.1.1 <0.2.0", + "dependencies": { + "assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "from": "assert-plus@>=1.0.0 <2.0.0" + } + } + }, + "har-schema": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-1.0.5.tgz", + "from": "har-schema@>=1.0.5 <2.0.0" + }, + "har-validator": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-4.2.1.tgz", + "from": "har-validator@>=4.2.1 <4.3.0" + }, + "hawk": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/hawk/-/hawk-3.1.3.tgz", + "from": "hawk@>=3.1.3 <3.2.0" + }, + "hoek": { + "version": "2.16.3", + "resolved": "https://registry.npmjs.org/hoek/-/hoek-2.16.3.tgz", + "from": "hoek@>=2.0.0 <3.0.0" + }, + "http-signature": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.1.1.tgz", + "from": "http-signature@>=1.1.0 <1.2.0" + }, + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "from": "inherits@>=2.0.1 <2.1.0" + }, + "is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "from": "is-typedarray@>=1.0.0 <1.1.0" + }, + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "from": "isarray@>=1.0.0 <1.1.0" + }, + "isstream": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", + "from": "isstream@>=0.1.2 <0.2.0" + }, + "jodid25519": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/jodid25519/-/jodid25519-1.0.2.tgz", + "from": "jodid25519@>=1.0.0 <2.0.0" + }, + "jsbn": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", + "from": "jsbn@>=0.1.0 <0.2.0" + }, + "json-schema": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", + "from": "json-schema@0.2.3" + }, + "json-stable-stringify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify/-/json-stable-stringify-1.0.1.tgz", + "from": "json-stable-stringify@>=1.0.1 <2.0.0" + }, + "json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "from": "json-stringify-safe@>=5.0.1 <5.1.0" + }, + "jsonify": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/jsonify/-/jsonify-0.0.0.tgz", + "from": "jsonify@>=0.0.0 <0.1.0" + }, + "jsprim": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.0.tgz", + "from": "jsprim@>=1.2.2 <2.0.0", + "dependencies": { + "assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "from": "assert-plus@1.0.0" + } + } + }, + "mime-db": { + "version": "1.27.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.27.0.tgz", + "from": "mime-db@>=1.27.0 <1.28.0" + }, + "mime-types": { + "version": "2.1.15", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.15.tgz", + "from": "mime-types@>=2.1.7 <2.2.0" + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "from": "ms@2.0.0" + }, + "oauth-sign": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.8.2.tgz", + "from": "oauth-sign@>=0.8.1 <0.9.0" + }, + "performance-now": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-0.2.0.tgz", + "from": "performance-now@>=0.2.0 <0.3.0" + }, + "process-nextick-args": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-1.0.7.tgz", + "from": "process-nextick-args@>=1.0.6 <1.1.0" + }, + "punycode": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", + "from": "punycode@>=1.4.1 <2.0.0" + }, + "qs": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.4.0.tgz", + "from": "qs@>=6.4.0 <6.5.0" + }, + "readable-stream": { + "version": "2.2.9", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.2.9.tgz", + "from": "readable-stream@>=0.3.0" + }, + "request": { + "version": "2.81.0", + "resolved": "https://registry.npmjs.org/request/-/request-2.81.0.tgz", + "from": "request@2.81.0" + }, + "safe-buffer": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.0.1.tgz", + "from": "safe-buffer@>=5.0.1 <6.0.0" + }, + "sntp": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/sntp/-/sntp-1.0.9.tgz", + "from": "sntp@>=1.0.0 <2.0.0" + }, + "sshpk": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.13.0.tgz", + "from": "sshpk@>=1.7.0 <2.0.0", + "dependencies": { + "assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "from": "assert-plus@>=1.0.0 <2.0.0" + } + } + }, + "stream-parser": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/stream-parser/-/stream-parser-0.3.1.tgz", + "from": "stream-parser@>=0.0.2" + }, + "string_decoder": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.0.1.tgz", + "from": "string_decoder@>=1.0.0 <1.1.0" + }, + "stringstream": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/stringstream/-/stringstream-0.0.5.tgz", + "from": "stringstream@>=0.0.4 <0.1.0" + }, + "throttle": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/throttle/-/throttle-1.0.3.tgz", + "from": "throttle@1.0.3" + }, + "tough-cookie": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.3.2.tgz", + "from": "tough-cookie@>=2.3.0 <2.4.0" + }, + "tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "from": "tunnel-agent@>=0.6.0 <0.7.0" + }, + "tweetnacl": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", + "from": "tweetnacl@>=0.14.0 <0.15.0" + }, + "util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "from": "util-deprecate@>=1.0.1 <1.1.0" + }, + "uuid": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.0.1.tgz", + "from": "uuid@>=3.0.0 <4.0.0" + }, + "verror": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.3.6.tgz", + "from": "verror@1.3.6" } } } diff --git a/.versions b/.versions index 6666981d..a93a8a78 100644 --- a/.versions +++ b/.versions @@ -36,7 +36,7 @@ npm-mongo@2.2.24 observe-sequence@1.0.16 ordered-dict@1.0.9 ostrio:cookies@2.2.1 -ostrio:files@1.7.15 +ostrio:files@1.7.16 promise@0.8.8 random@1.0.10 reactive-var@1.0.11 diff --git a/docs/aws-s3-integration.md b/docs/aws-s3-integration.md index fab58131..6cef4181 100644 --- a/docs/aws-s3-integration.md +++ b/docs/aws-s3-integration.md @@ -58,6 +58,7 @@ import { Meteor } from 'meteor/meteor'; import { _ } from 'meteor/underscore'; import { Random } from 'meteor/random'; import { FilesCollection } from 'meteor/ostrio:files'; +import stream from 'stream'; import S3 from 'aws-sdk/clients/s3'; // http://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/S3.html // See fs-extra and graceful-fs NPM packages @@ -82,7 +83,11 @@ if (s3Conf && s3Conf.key && s3Conf.secret && s3Conf.bucket && s3Conf.region) { secretAccessKey: s3Conf.secret, accessKeyId: s3Conf.key, region: s3Conf.region, - sslEnabled: true + // sslEnabled: true, // optional + httpOptions: { + timeout: 6000, + agent: false + } }); // Declare the Meteor file collection on the Server @@ -108,8 +113,8 @@ if (s3Conf && s3Conf.key && s3Conf.secret && s3Conf.bucket && s3Conf.region) { // Key is the file name we are creating on AWS:S3, so it will be like files/XXXXXXXXXXXXXXXXX-original.XXXX // Body is the file stream we are sending to AWS s3.putObject({ - ServerSideEncryption: 'AES256', - StorageClass: 'STANDARD_IA', + // ServerSideEncryption: 'AES256', // Optional + StorageClass: 'STANDARD', Bucket: s3Conf.bucket, Key: filePath, Body: fs.createReadStream(vRef.path), @@ -150,24 +155,54 @@ if (s3Conf && s3Conf.key && s3Conf.secret && s3Conf.bucket && s3Conf.region) { } if (path) { - //pipe the request from S3 through us and direct into the http socket - //need to set the header so the file shows up properly - - //this is needed if you want to view the files in the browser - //set the content type and lenth to what we recorded - http.response.setHeader('content-type', fileRef.type || 'application/pdf'); - http.response.setHeader('content-length', fileRef.size || 0); - - - //get the file/key and pipe it into out http response - s3.getObject({ + // If file is successfully moved to AWS:S3 + // We will pipe request to AWS:S3 + // So, original link will stay always secure + + // To force ?play and ?download parameters + // and to keep original file name, content-type, + // content-disposition, chunked "streaming" and cache-control + // we're using low-level .serve() method + const opts = { Bucket: s3Conf.bucket, Key: path - }).createReadStream().on('error', (err) => { - bound(() => { - console.error(err); - }); - }).pipe(http.response); + }; + + if (http.request.headers.range) { + const vRef = fileRef.versions[version]; + let range = _.clone(http.request.headers.range); + const array = range.split(/bytes=([0-9]*)-([0-9]*)/); + const start = parseInt(array[1]); + let end = parseInt(array[2]); + if (isNaN(end)) { + // Request data from AWS:S3 by small chunks + end = (start + this.chunkSize) - 1; + if (end >= vRef.size) { + end = vRef.size - 1; + } + } + opts.Range = `bytes=${start}-${end}`; + http.request.headers.range = `bytes=${start}-${end}`; + } + + const fileColl = this; + s3.getObject(opts, function (error) { + if (error) { + console.error(error); + if (!http.response.finished) { + http.response.end(); + } + } else { + if (http.request.headers.range && this.httpResponse.headers['content-range']) { + // Set proper range header in according to what is returned from AWS:S3 + http.request.headers.range = this.httpResponse.headers['content-range'].split('/')[0].replace('bytes ', 'bytes='); + } + + const dataStream = new stream.PassThrough(); + fileColl.serve(http, fileRef, fileRef.versions[version], version, dataStream); + dataStream.end(this.data.Body); + } + }); return true; } diff --git a/files.coffee b/files.coffee index 5dd05ebb..27dc5fad 100755 --- a/files.coffee +++ b/files.coffee @@ -5,12 +5,12 @@ if Meteor.isServer ### @summary Require NPM packages ### - fs = require 'fs-extra' - events = require 'events' - request = require 'request' - Throttle = require 'throttle' - fileType = require 'file-type' - nodePath = require 'path' + fs = Npm.require 'fs-extra' + events = Npm.require 'events' + request = Npm.require 'request' + Throttle = Npm.require 'throttle' + fileType = Npm.require 'file-type' + nodePath = Npm.require 'path' ### @var {Object} bound - Meteor.bindEnvironment (Fiber wrapper) @@ -758,10 +758,12 @@ class FilesCollection console.warn '[FilesCollection._checkAccess] WARN: Access denied!' if self.debug if http text = 'Access denied!' - http.response.writeHead rc, - 'Content-Length': text.length - 'Content-Type': 'text/plain' - http.response.end text + if !http.response.headersSent + http.response.writeHead rc, + 'Content-Length': text.length + 'Content-Type': 'text/plain' + if !http.response.finished + http.response.end text return false else return true @@ -782,8 +784,10 @@ class FilesCollection handleError = (error) -> console.warn "[FilesCollection] [Upload] [HTTP] Exception:", error - response.writeHead 500 - response.end JSON.stringify {error} + if !response.headersSent + response.writeHead 500 + if !response.finished + response.end JSON.stringify {error} return body = '' @@ -820,19 +824,29 @@ class FilesCollection if opts.eof self._handleUpload result, opts, -> - response.writeHead 200 + if !response.headersSent + response.writeHead 200 result.file.meta = fixJSONStringify result.file.meta if result?.file?.meta - response.end JSON.stringify result + if !response.finished + response.end JSON.stringify result return return else self.emit '_handleUpload', result, opts, NOOP - response.writeHead 204 - response.end() + if !response.headersSent + response.writeHead 204 + if !response.finished + response.end() else - opts = JSON.parse body + try + opts = JSON.parse body + catch e + console.error 'Can\'t parse incoming JSON from Client on [.insert() | upload], something went wrong!' + console.error e + opts = file: {} + opts.___s = true console.info "[FilesCollection] [File Start HTTP] #{opts.file.name} - #{opts.fileId}" if self.debug opts.file.meta = fixJSONParse opts.file.meta if opts?.file?.meta @@ -845,14 +859,18 @@ class FilesCollection self._createStream result._id, result.path, _.omit(opts, '___s') if opts.returnMeta - response.writeHead 200 - response.end JSON.stringify { - uploadRoute: "#{self.downloadRoute}/#{self.collectionName}/__upload" - file: result - } + if !response.headersSent + response.writeHead 200 + if !response.finished + response.end JSON.stringify { + uploadRoute: "#{self.downloadRoute}/#{self.collectionName}/__upload" + file: result + } else - response.writeHead 204 - response.end() + if !response.headersSent + response.writeHead 204 + if !response.finished + response.end() catch error handleError error return @@ -1857,7 +1875,11 @@ class FilesCollection 'x-fileId': opts.fileId 'content-type': 'text/plain' }, (error, result) -> - result = JSON.parse result?.content or {} + try + result = JSON.parse result?.content or {} + catch e + console.warn 'Something went wrong! [sendEOF] method doesn\'t returned JSON! Looks like you\'re on Cordova app or behind proxy, switching to DDP transport is recommended.' + result = {} result.meta = fixJSONParse result.meta if result?.meta self.emitEvent 'end', [error, result] return @@ -2271,10 +2293,13 @@ class FilesCollection _404: if Meteor.isServer then (http) -> console.warn "[FilesCollection] [download(#{http.request.originalUrl})] [_404] File not found" if @debug text = 'File Not Found :(' - http.response.writeHead 404, - 'Content-Length': text.length - 'Content-Type': 'text/plain' - http.response.end text + + if !http.response.headersSent + http.response.writeHead 404, + 'Content-Length': text.length + 'Content-Type': 'text/plain' + if !http.response.finished + http.response.end text return else undefined @@ -2348,10 +2373,11 @@ class FilesCollection else dispositionType = 'inline; ' - dispositionName = "filename=\"#{encodeURIComponent(fileRef.name)}\"; filename=*UTF-8\"#{encodeURIComponent(fileRef.name)}\"; " - dispositionEncoding = 'charset=utf-8' + dispositionName = "filename=\"#{encodeURI(fileRef.name)}\"; filename*=UTF-8''#{encodeURI(fileRef.name)}; " + dispositionEncoding = 'charset=UTF-8' - http.response.setHeader 'Content-Disposition', dispositionType + dispositionName + dispositionEncoding + if !http.response.headersSent + http.response.setHeader 'Content-Disposition', dispositionType + dispositionName + dispositionEncoding if http.request.headers.range and not force200 partiral = true @@ -2384,61 +2410,109 @@ class FilesCollection responseType = '200' streamErrorHandler = (error) -> - http.response.writeHead 500 - http.response.end error.toString() console.error "[FilesCollection] [serve(#{vRef.path}, #{version})] [500]", error if self.debug + if !http.response.finished + http.response.end error.toString() return headers = if _.isFunction(self.responseHeaders) then self.responseHeaders(responseType, fileRef, vRef, version) else self.responseHeaders unless headers['Cache-Control'] - http.response.setHeader 'Cache-Control', self.cacheControl + if !http.response.headersSent + http.response.setHeader 'Cache-Control', self.cacheControl for key, value of headers - http.response.setHeader key, value + if !http.response.headersSent + http.response.setHeader key, value switch responseType when '400' console.warn "[FilesCollection] [serve(#{vRef.path}, #{version})] [400] Content-Length mismatch!" if self.debug text = 'Content-Length mismatch!' - http.response.writeHead 400, - 'Content-Type': 'text/plain' - 'Content-Length': text.length - http.response.end text + + if !http.response.headersSent + http.response.writeHead 400, + 'Content-Type': 'text/plain' + 'Content-Length': text.length + if !http.response.finished + http.response.end text break when '404' return self._404 http break when '416' console.warn "[FilesCollection] [serve(#{vRef.path}, #{version})] [416] Content-Range is not specified!" if self.debug - http.response.writeHead 416 - http.response.end() + if !http.response.headersSent + http.response.writeHead 416 + if !http.response.finished + http.response.end() break when '200' console.info "[FilesCollection] [serve(#{vRef.path}, #{version})] [200]" if self.debug stream = readableStream or fs.createReadStream vRef.path - http.response.writeHead 200 if readableStream + if !http.response.headersSent + http.response.writeHead 200 if readableStream + + http.response.on 'close', -> + stream.abort?() + stream.end?() + return + + http.request.on 'abort', -> + stream.abort?() + stream.end?() + return + stream.on('open', -> - http.response.writeHead 200 + if !http.response.headersSent + http.response.writeHead 200 + return + ).on('abort', -> + if !http.response.finished + http.response.end() + if !http.request.aborted + http.request.abort() return ).on('error', streamErrorHandler ).on 'end', -> - http.response.end() + if !http.response.finished + http.response.end() return stream.pipe new Throttle {bps: self.throttle, chunksize: self.chunkSize} if self.throttle stream.pipe http.response break when '206' console.info "[FilesCollection] [serve(#{vRef.path}, #{version})] [206]" if self.debug - http.response.setHeader 'Content-Range', "bytes #{reqRange.start}-#{reqRange.end}/#{vRef.size}" + if !http.response.headersSent + http.response.setHeader 'Content-Range', "bytes #{reqRange.start}-#{reqRange.end}/#{vRef.size}" stream = readableStream or fs.createReadStream vRef.path, {start: reqRange.start, end: reqRange.end} - http.response.writeHead 206 if readableStream + if !http.response.headersSent + http.response.writeHead 206 if readableStream + + http.response.on 'close', -> + stream.abort?() + stream.end?() + return + + http.request.on 'abort', -> + stream.abort?() + stream.end?() + return + stream.on('open', -> - http.response.writeHead 206 + if !http.response.headersSent + http.response.writeHead 206 + return + ).on('abort', -> + if !http.response.finished + http.response.end() + if !http.request.aborted + http.request.abort() return ).on('error', streamErrorHandler ).on 'end', -> - http.response.end() + if !http.response.finished + http.response.end() return stream.pipe new Throttle {bps: self.throttle, chunksize: self.chunkSize} if self.throttle stream.pipe http.response diff --git a/package.js b/package.js index 815a688c..67f52d70 100755 --- a/package.js +++ b/package.js @@ -1,6 +1,6 @@ Package.describe({ name: 'ostrio:files', - version: '1.7.15', + version: '1.7.16', summary: 'File upload via DDP/HTTP to server FS, AWS, GridFS, DropBox, Google Drive or other 3rd party storage', git: 'https://github.com/VeliovGroup/Meteor-Files', documentation: 'README.md'