Fixed Non-ASCII attachment filename will crash when downloading.

Thanks to xet7 !

Fixes #2759
This commit is contained in:
Lauri Ojansivu 2021-04-29 13:26:49 +03:00
parent 843ff8eaaa
commit c2da477735
277 changed files with 30568 additions and 52 deletions

View file

@ -0,0 +1,5 @@
language: node_js
node_js:
- "0.10"
before_install:
- "curl -L http://git.io/s0Zu-w | /bin/sh"

View file

@ -0,0 +1,288 @@
# Changelog
## [v0.1.50] (https://github.com/zcfs/Meteor-cfs-access-point/tree/v0.1.46)
#### 21/1/19 by Harry Adel
- Bump to version 0.1.50
- *Merged pull-request:* "filename conversion for FS.HTTP.Handlers.Get" [#9](https://github.com/zcfs/Meteor-CollectionFS/pull/994) ([yatusiter](https://github.com/yatusiter))
## [v0.1.46] (https://github.com/zcfs/Meteor-cfs-access-point/tree/v0.1.46)
#### 30/3/15 by Eric Dobbertin
- Bump to version 0.1.46
- *Merged pull-request:* [#611](https://github.com/zcfs/Meteor-CollectionFS/issues/611)
- Exposed request handlers on `FS.HTTP.Handlers` object so that app can override
## [v0.1.43] (https://github.com/zcfs/Meteor-cfs-access-point/tree/v0.1.43)
#### 20/12/14 by Morten Henriksen
- add changelog
- Bump to version 0.1.43
- *Fixed bug:* "Doesn't work in IE 8" [#10](https://github.com/zcfs/Meteor-cfs-access-point/issues/10)
- *Merged pull-request:* "rootUrlPathPrefix fix for cordova" [#9](https://github.com/zcfs/Meteor-cfs-access-point/issues/9) ([dmitriyles](https://github.com/dmitriyles))
- *Merged pull-request:* "Support for expiration token" [#1](https://github.com/zcfs/Meteor-cfs-access-point/issues/1) ([tanis2000](https://github.com/tanis2000))
Patches by GitHub users [@dmitriyles](https://github.com/dmitriyles), [@tanis2000](https://github.com/tanis2000).
## [v0.1.42] (https://github.com/zcfs/Meteor-cfs-access-point/tree/v0.1.42)
#### 17/12/14 by Morten Henriksen
## [v0.1.41] (https://github.com/zcfs/Meteor-cfs-access-point/tree/v0.1.41)
#### 17/12/14 by Morten Henriksen
- mbr update, remove versions.json
- Cordova rootUrlPathPrefix fix
- Bump to version 0.1.41
## [v0.1.40] (https://github.com/zcfs/Meteor-cfs-access-point/tree/v0.1.40)
#### 17/12/14 by Morten Henriksen
- mbr fixed warnings
- fixes to GET handler
- add back tests
- support apps in server subdirectories; closes #8
- 0.9.1 support
## [v0.0.39] (https://github.com/zcfs/Meteor-cfs-access-point/tree/v0.0.39)
#### 28/08/14 by Morten Henriksen
- Meteor Package System Update
## [v0.0.38] (https://github.com/zcfs/Meteor-cfs-access-point/tree/v0.0.38)
#### 27/08/14 by Eric Dobbertin
## [v0.0.37] (https://github.com/zcfs/Meteor-cfs-access-point/tree/v0.0.37)
#### 26/08/14 by Eric Dobbertin
- change package name to lowercase
## [v0.0.36] (https://github.com/zcfs/Meteor-cfs-access-point/tree/v0.0.36)
#### 06/08/14 by Eric Dobbertin
- pass correct arg
## [v0.0.35] (https://github.com/zcfs/Meteor-cfs-access-point/tree/v0.0.35)
#### 06/08/14 by Eric Dobbertin
- move to correct place
## [v0.0.34] (https://github.com/zcfs/Meteor-cfs-access-point/tree/v0.0.34)
#### 05/08/14 by Eric Dobbertin
- *Merged pull-request:* "Added contentLength for ranges and inline content" [#5](https://github.com/zcfs/Meteor-cfs-access-point/issues/5) ([maomorales](https://github.com/maomorales))
- Content-Length and Last-Modified headers
Patches by GitHub user [@maomorales](https://github.com/maomorales).
## [v0.0.33] (https://github.com/zcfs/Meteor-cfs-access-point/tree/v0.0.33)
#### 31/07/14 by Eric Dobbertin
- *Merged pull-request:* "Force browser to download with filename passed in url" [#3](https://github.com/zcfs/Meteor-cfs-access-point/issues/3) ([elbowz](https://github.com/elbowz))
- Force browser to download with filename passed in url
Patches by GitHub user [@elbowz](https://github.com/elbowz).
## [v0.0.32] (https://github.com/zcfs/Meteor-cfs-access-point/tree/v0.0.32)
#### 28/07/14 by Eric Dobbertin
- support collection-specific GET headers
- update API docs
## [v0.0.31] (https://github.com/zcfs/Meteor-cfs-access-point/tree/v0.0.31)
#### 06/07/14 by Eric Dobbertin
- allow override filename
## [v0.0.30] (https://github.com/zcfs/Meteor-cfs-access-point/tree/v0.0.30)
#### 30/04/14 by Eric Dobbertin
- ignore auth on server so that url method can be called on the server
## [v0.0.29] (https://github.com/zcfs/Meteor-cfs-access-point/tree/v0.0.29)
#### 30/04/14 by Eric Dobbertin
- rework the new authtoken stuff to make it easier to debug and cleaner
## [v0.0.28] (https://github.com/zcfs/Meteor-cfs-access-point/tree/v0.0.28)
#### 29/04/14 by Eric Dobbertin
- generate api docs
- adjustments to use new FS.File API functions, plus have `url` function omit query string whenever possible
- *Merged pull-request:* "Support for expiration token" [#1](https://github.com/zcfs/Meteor-cfs-access-point/issues/1) ([tanis2000](https://github.com/tanis2000))
- Switched to HTTP.call() to get the server time
- Better check for options.auth being a number. Check to see if we have Buffer() available on the server side. New check to make sure we have the token. Switched Metheor.method to HTTP.methods for the getServerTime() function.
- Expiration is now optional. If auth is set to a number, that is the number of seconds the token is valid for.
- Added time sync with the server for token generation.
- Added code to pass a token with a set expiration date from the client. Added token check on the server side.
Patches by GitHub user [@tanis2000](https://github.com/tanis2000).
## [v0.0.27] (https://github.com/zcfs/Meteor-cfs-access-point/tree/v0.0.27)
#### 08/04/14 by Eric Dobbertin
- clean up/fix whole-file upload handler
## [v0.0.26] (https://github.com/zcfs/Meteor-cfs-access-point/tree/v0.0.26)
#### 07/04/14 by Eric Dobbertin
- add URL options to get temporary images while uploading and storing
## [v0.0.25] (https://github.com/zcfs/Meteor-cfs-access-point/tree/v0.0.25)
#### 03/04/14 by Eric Dobbertin
- * allow `setBaseUrl` to be called either outside of Meteor.startup or inside * move encodeParams helper to FS.Utility
## [v0.0.24] (https://github.com/zcfs/Meteor-cfs-access-point/tree/v0.0.24)
#### 03/04/14 by Eric Dobbertin
- properly remount URLs
- when uploading chunks, check the insert allow/deny since it's part of inserting
## [v0.0.23] (https://github.com/zcfs/Meteor-cfs-access-point/tree/v0.0.23)
#### 31/03/14 by Eric Dobbertin
- use latest releases
## [v0.0.22] (https://github.com/zcfs/Meteor-cfs-access-point/tree/v0.0.22)
#### 29/03/14 by Morten Henriksen
- remove underscore deps
## [v0.0.21] (https://github.com/zcfs/Meteor-cfs-access-point/tree/v0.0.21)
#### 25/03/14 by Morten Henriksen
- add comments about shareId
## [v0.0.20] (https://github.com/zcfs/Meteor-cfs-access-point/tree/v0.0.20)
#### 23/03/14 by Morten Henriksen
- Rollback to specific git dependency
- Try modified test script
- deps are already in collectionFS
## [v0.0.19] (https://github.com/zcfs/Meteor-cfs-access-point/tree/v0.0.19)
#### 22/03/14 by Morten Henriksen
- try to fix travis test by using general package references
## [v0.0.18] (https://github.com/zcfs/Meteor-cfs-access-point/tree/v0.0.18)
#### 22/03/14 by Morten Henriksen
- If the read stream fails we send an error to the client
## [v0.0.17] (https://github.com/zcfs/Meteor-cfs-access-point/tree/v0.0.17)
#### 21/03/14 by Morten Henriksen
- remove smart lock
- commit smart.lock, trying to get tests to pass on travis
- some minor pkg adjustments; trying to get tests to pass on travis
## [v0.0.16] (https://github.com/zcfs/Meteor-cfs-access-point/tree/v0.0.16)
#### 18/03/14 by Morten Henriksen
- Rollback to using the direct storage adapter - makes more sense when serving files
- shift to new http.methods streaming api
- move server side DDP access points to cfs-download-ddp pkg; update API docs
- fix typo...
- return something useful
- convert to streaming
- Add streaming WIP
- fix/adjust some tests; minor improvements to some handlers
- Add unmount and allow mount to use default selector function
- Refactor access point - wip
## [v0.0.15] (https://github.com/zcfs/Meteor-cfs-access-point/tree/v0.0.15)
#### 05/03/14 by Morten Henriksen
- Refactor note, encode stuff should be prefixed into FS.Utility
- FS.File.url add user deps when auth is used
- fix url method
- query string fix
- move PUT access points for HTTP upload into this package; mount DELETE on /record/ as well as /files/; some fixes and improvements to handlers
## [v0.0.14] (https://github.com/zcfs/Meteor-cfs-access-point/tree/v0.0.14)
#### 03/03/14 by Eric Dobbertin
- better error; return Buffer instead of converting to Uint8Array
## [v0.0.13] (https://github.com/zcfs/Meteor-cfs-access-point/tree/v0.0.13)
#### 02/03/14 by Eric Dobbertin
- more tests, make everything work, add unpublish method
- Merge branch 'master' of https://github.com/zcfs/Meteor-cfs-access-point
## [v0.0.12] (https://github.com/zcfs/Meteor-cfs-access-point/tree/v0.0.12)
#### 01/03/14 by Eric Dobbertin
- add travis-ci image
- rework URLs a bit, use http-publish package to publish FS.Collection listing, and add a test for this (!)
- add http-publish dependency
- del should be delete
## [v0.0.11] (https://github.com/zcfs/Meteor-cfs-access-point/tree/v0.0.11)
#### 28/02/14 by Eric Dobbertin
- move some code to other packages; redo the HTTP GET/DEL methods
## [v0.0.10] (https://github.com/zcfs/Meteor-cfs-access-point/tree/v0.0.10)
#### 28/02/14 by Eric Dobbertin
- move DDP upload methods to new cfs-upload-ddp package
## [v0.0.9] (https://github.com/zcfs/Meteor-cfs-access-point/tree/v0.0.9)
#### 21/02/14 by Eric Dobbertin
- new URL syntax; use the store's file key instead of ID; also fix allow/deny checks with insecure
## [v0.0.8] (https://github.com/zcfs/Meteor-cfs-access-point/tree/v0.0.8)
#### 20/02/14 by Eric Dobbertin
- support HTTP PUT of new file and fix PUT of existing file
## [v0.0.7] (https://github.com/zcfs/Meteor-cfs-access-point/tree/v0.0.7)
#### 17/02/14 by Morten Henriksen
- add http-methods dependency
## [v0.0.6] (https://github.com/zcfs/Meteor-cfs-access-point/tree/v0.0.6)
#### 16/02/14 by Morten Henriksen
## [v0.0.5] (https://github.com/zcfs/Meteor-cfs-access-point/tree/v0.0.5)
#### 16/02/14 by Morten Henriksen
- a few fixes and improvements
- need to actually mount it
- attempt at switching to generic HTTP access point; also add support for chunked http downloads (range header)
## [v0.0.4] (https://github.com/zcfs/Meteor-cfs-access-point/tree/v0.0.4)
#### 15/02/14 by Morten Henriksen
- Merge branch 'master' of https://github.com/zcfs/Meteor-cfs-access-point
- corrected typo
- added debugging
- call HTTP.methods on server only
- run client side, too, for side effects
- rework for additional abstraction; also DDP methods don't need to be per-collection so they no longer are
## [v0.0.3] (https://github.com/zcfs/Meteor-cfs-access-point/tree/v0.0.3)
#### 13/02/14 by Morten Henriksen
## [v0.0.2] (https://github.com/zcfs/Meteor-cfs-access-point/tree/v0.0.2)
#### 13/02/14 by Morten Henriksen
## [v0.0.1] (https://github.com/zcfs/Meteor-cfs-access-point/tree/v0.0.1)
#### 13/02/14 by Morten Henriksen
- init commit

View file

@ -0,0 +1,20 @@
The MIT License (MIT)
Copyright (c) 2013 [@raix](https://github.com/raix) and [@aldeed](https://github.com/aldeed), aka Morten N.O. Nørgaard Henriksen, mh@gi-software.com
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View file

@ -0,0 +1,32 @@
wekan-cfs-access-point [![Build Status](https://travis-ci.org/CollectionFS/Meteor-cfs-access-point.png?branch=master)](https://travis-ci.org/CollectionFS/Meteor-cfs-access-point)
=========================
This is a Meteor package used by
[CollectionFS](https://github.com/zcfs/Meteor-CollectionFS).
You don't need to manually add this package to your app. It is added when you
add the `wekan-cfs-standard-packages` package. You could potentially use your own access point
package instead.
## Define a URL for Collection Listing
To define a URL that accepts GET requests and returns a list of published
files in a FS.Collection:
```js
Images = new FS.Collection("images", {
stores: [myStore]
});
FS.HTTP.publish(Images, function () {
// `this` provides a context similar to Meteor.publish
return Images.find();
});
```
The URL will be '/cfs/record/images', where the `cfs` piece is configurable
using the `FS.HTTP.setBaseUrl` method.
## API Documentation
[Here](api.md)

View file

@ -0,0 +1,58 @@
FS.HTTP.setHeadersForGet = function setHeadersForGet() {
// Client Stub
};
FS.HTTP.now = function() {
return new Date(new Date() + FS.HTTP._serverTimeDiff);
};
// Returns the localstorage if its found and working
// TODO: check if this works in IE
// could use Meteor._localStorage - just needs a rewrite
FS.HTTP._storage = function() {
var storage,
fail,
uid;
try {
uid = "test";
(storage = window.localStorage).setItem(uid, uid);
fail = (storage.getItem(uid) !== uid);
storage.removeItem(uid);
if (fail) {
storage = false;
}
} catch(e) {
console.log("Error initializing storage for FS.HTTP");
console.log(e);
}
return storage;
};
// get our storage if found
FS.HTTP.storage = FS.HTTP._storage();
FS.HTTP._prefix = 'fsHTTP.';
FS.HTTP._serverTimeDiff = 0; // Time difference in ms
if (FS.HTTP.storage) {
// Initialize the FS.HTTP._serverTimeDiff
FS.HTTP._serverTimeDiff = (1*FS.HTTP.storage.getItem(FS.HTTP._prefix+'timeDiff')) || 0;
// At client startup we figure out the time difference between server and
// client time - this includes lag and timezone
Meteor.startup(function() {
// Call the server method an get server time
HTTP.get(rootUrlPathPrefix + '/cfs/servertime', function(error, result) {
if (!error) {
// Update our server time diff
var dateNew = new Date(+result.content);
FS.HTTP._serverTimeDiff = dateNew - new Date();// - lag or/and timezone
// Update the localstorage
FS.HTTP.storage.setItem(FS.HTTP._prefix + 'timeDiff', FS.HTTP._serverTimeDiff);
} else {
console.log(error.message);
}
}); // EO Server call
});
}

View file

@ -0,0 +1,199 @@
rootUrlPathPrefix = __meteor_runtime_config__.ROOT_URL_PATH_PREFIX || "";
// Adjust the rootUrlPathPrefix if necessary
if (rootUrlPathPrefix.length > 0) {
if (rootUrlPathPrefix.slice(0, 1) !== '/') {
rootUrlPathPrefix = '/' + rootUrlPathPrefix;
}
if (rootUrlPathPrefix.slice(-1) === '/') {
rootUrlPathPrefix = rootUrlPathPrefix.slice(0, -1);
}
}
// prepend ROOT_URL when isCordova
if (Meteor.isCordova) {
rootUrlPathPrefix = Meteor.absoluteUrl(rootUrlPathPrefix.replace(/^\/+/, '')).replace(/\/+$/, '');
}
baseUrl = '/cfs';
FS.HTTP = FS.HTTP || {};
// Note the upload URL so that client uploader packages know what it is
FS.HTTP.uploadUrl = rootUrlPathPrefix + baseUrl + '/files';
/**
* @method FS.HTTP.setBaseUrl
* @public
* @param {String} newBaseUrl - Change the base URL for the HTTP GET and DELETE endpoints.
* @returns {undefined}
*/
FS.HTTP.setBaseUrl = function setBaseUrl(newBaseUrl) {
// Adjust the baseUrl if necessary
if (newBaseUrl.slice(0, 1) !== '/') {
newBaseUrl = '/' + newBaseUrl;
}
if (newBaseUrl.slice(-1) === '/') {
newBaseUrl = newBaseUrl.slice(0, -1);
}
// Update the base URL
baseUrl = newBaseUrl;
// Change the upload URL so that client uploader packages know what it is
FS.HTTP.uploadUrl = rootUrlPathPrefix + baseUrl + '/files';
// Remount URLs with the new baseUrl, unmounting the old, on the server only.
// If existingMountPoints is empty, then we haven't run the server startup
// code yet, so this new URL will be used at that point for the initial mount.
if (Meteor.isServer && !FS.Utility.isEmpty(_existingMountPoints)) {
mountUrls();
}
};
/*
* FS.File extensions
*/
/**
* @method FS.File.prototype.urlRelative Construct the file url
* @public
* @param {Object} [options]
* @param {String} [options.store] Name of the store to get from. If not defined, the first store defined in `options.stores` for the collection on the client is used.
* @param {Boolean} [options.auth=null] Add authentication token to the URL query string? By default, a token for the current logged in user is added on the client. Set this to `false` to omit the token. Set this to a string to provide your own token. Set this to a number to specify an expiration time for the token in seconds.
* @param {Boolean} [options.download=false] Should headers be set to force a download? Typically this means that clicking the link with this URL will download the file to the user's Downloads folder instead of displaying the file in the browser.
* @param {Boolean} [options.brokenIsFine=false] Return the URL even if we know it's currently a broken link because the file hasn't been saved in the requested store yet.
* @param {Boolean} [options.returnWhenStored=false] Flag relevant only on server, Return the URL only when file has been saved to the requested store.
* @param {Boolean} [options.metadata=false] Return the URL for the file metadata access point rather than the file itself.
* @param {String} [options.uploading=null] A URL to return while the file is being uploaded.
* @param {String} [options.storing=null] A URL to return while the file is being stored.
* @param {String} [options.filename=null] Override the filename that should appear at the end of the URL. By default it is the name of the file in the requested store.
*
* Returns the relative HTTP URL for getting the file or its metadata.
*/
FS.File.prototype.urlRelative = function(options) {
var self = this;
options = options || {};
options = FS.Utility.extend({
store: null,
auth: null,
download: false,
metadata: false,
brokenIsFine: false,
returnWhenStored: false,
uploading: null, // return this URL while uploading
storing: null, // return this URL while storing
filename: null // override the filename that is shown to the user
}, options.hash || options); // check for "hash" prop if called as helper
// Primarily useful for displaying a temporary image while uploading an image
if (options.uploading && !self.isUploaded()) {
return options.uploading;
}
if (self.isMounted()) {
// See if we've stored in the requested store yet
var storeName = options.store || self.collection.primaryStore.name;
if (!self.hasStored(storeName)) {
if (options.storing) {
return options.storing;
} else if (!options.brokenIsFine) {
// In case we want to get back the url only when he is stored
if (Meteor.isServer && options.returnWhenStored) {
// Wait till file is stored to storeName
self.onStored(storeName);
} else {
// We want to return null if we know the URL will be a broken
// link because then we can avoid rendering broken links, broken
// images, etc.
return null;
}
}
}
// Add filename to end of URL if we can determine one
var filename = options.filename || self.name({store: storeName});
if (typeof filename === "string" && filename.length) {
filename = '/' + filename;
} else {
filename = '';
}
// TODO: Could we somehow figure out if the collection requires login?
var authToken = '';
if (Meteor.isClient && typeof Accounts !== "undefined" && typeof Accounts._storedLoginToken === "function") {
if (options.auth !== false) {
// Add reactive deps on the user
Meteor.userId();
var authObject = {
authToken: Accounts._storedLoginToken() || ''
};
// If it's a number, we use that as the expiration time (in seconds)
if (options.auth === +options.auth) {
authObject.expiration = FS.HTTP.now() + options.auth * 1000;
}
// Set the authToken
var authString = JSON.stringify(authObject);
authToken = FS.Utility.btoa(authString);
}
} else if (typeof options.auth === "string") {
// If the user supplies auth token the user will be responsible for
// updating
authToken = options.auth;
}
// Construct query string
var params = {};
if (authToken !== '') {
params.token = authToken;
}
if (options.download) {
params.download = true;
}
if (options.store) {
// We use options.store here instead of storeName because we want to omit the queryString
// whenever possible, allowing users to have "clean" URLs if they want. The server will
// assume the first store defined on the server, which means that we are assuming that
// the first on the client is also the first on the server. If that's not the case, the
// store option should be supplied.
params.store = options.store;
}
var queryString = FS.Utility.encodeParams(params);
if (queryString.length) {
queryString = '?' + queryString;
}
// Determine which URL to use
var area;
if (options.metadata) {
area = '/record';
} else {
area = '/files';
}
// Construct and return the http method url
return baseUrl + area + '/' + self.collection.name + '/' + self._id + filename + queryString;
}
};
/**
* @method FS.File.prototype.url Construct the file url
* @public
* @param {Object} [options]
* @param {String} [options.store] Name of the store to get from. If not defined, the first store defined in `options.stores` for the collection on the client is used.
* @param {Boolean} [options.auth=null] Add authentication token to the URL query string? By default, a token for the current logged in user is added on the client. Set this to `false` to omit the token. Set this to a string to provide your own token. Set this to a number to specify an expiration time for the token in seconds.
* @param {Boolean} [options.download=false] Should headers be set to force a download? Typically this means that clicking the link with this URL will download the file to the user's Downloads folder instead of displaying the file in the browser.
* @param {Boolean} [options.brokenIsFine=false] Return the URL even if we know it's currently a broken link because the file hasn't been saved in the requested store yet.
* @param {Boolean} [options.metadata=false] Return the URL for the file metadata access point rather than the file itself.
* @param {String} [options.uploading=null] A URL to return while the file is being uploaded.
* @param {String} [options.storing=null] A URL to return while the file is being stored.
* @param {String} [options.filename=null] Override the filename that should appear at the end of the URL. By default it is the name of the file in the requested store.
*
* Returns the HTTP URL for getting the file or its metadata.
*/
FS.File.prototype.url = function(options) {
self = this;
return rootUrlPathPrefix + self.urlRelative(options);
};

View file

@ -0,0 +1,307 @@
getHeaders = [];
getHeadersByCollection = {};
var contentDisposition = Npm.require('content-disposition');
FS.HTTP.Handlers = {};
/**
* @method FS.HTTP.Handlers.Del
* @public
* @returns {any} response
*
* HTTP DEL request handler
*/
FS.HTTP.Handlers.Del = function httpDelHandler(ref) {
var self = this;
var opts = FS.Utility.extend({}, self.query || {}, self.params || {});
// If DELETE request, validate with 'remove' allow/deny, delete the file, and return
FS.Utility.validateAction(ref.collection.files._validators['remove'], ref.file, self.userId);
/*
* From the DELETE spec:
* A successful response SHOULD be 200 (OK) if the response includes an
* entity describing the status, 202 (Accepted) if the action has not
* yet been enacted, or 204 (No Content) if the action has been enacted
* but the response does not include an entity.
*/
self.setStatusCode(200);
return {
deleted: !!ref.file.remove()
};
};
/**
* @method FS.HTTP.Handlers.GetList
* @public
* @returns {Object} response
*
* HTTP GET file list request handler
*/
FS.HTTP.Handlers.GetList = function httpGetListHandler() {
// Not Yet Implemented
// Need to check publications and return file list based on
// what user is allowed to see
};
/*
requestRange will parse the range set in request header - if not possible it
will throw fitting errors and autofill range for both partial and full ranges
throws error or returns the object:
{
start
end
length
unit
partial
}
*/
var requestRange = function(req, fileSize) {
if (req) {
if (req.headers) {
var rangeString = req.headers.range;
// Make sure range is a string
if (rangeString === ''+rangeString) {
// range will be in the format "bytes=0-32767"
var parts = rangeString.split('=');
var unit = parts[0];
// Make sure parts consists of two strings and range is of type "byte"
if (parts.length == 2 && unit == 'bytes') {
// Parse the range
var range = parts[1].split('-');
var start = Number(range[0]);
var end = Number(range[1]);
// Fix invalid ranges?
if (range[0] != start) start = 0;
if (range[1] != end || !end) end = fileSize - 1;
// Make sure range consists of a start and end point of numbers and start is less than end
if (start < end) {
var partSize = 0 - start + end + 1;
// Return the parsed range
return {
start: start,
end: end,
length: partSize,
size: fileSize,
unit: unit,
partial: (partSize < fileSize)
};
} else {
throw new Meteor.Error(416, "Requested Range Not Satisfiable");
}
} else {
// The first part should be bytes
throw new Meteor.Error(416, "Requested Range Unit Not Satisfiable");
}
} else {
// No range found
}
} else {
// throw new Error('No request headers set for _parseRange function');
}
} else {
throw new Error('No request object passed to _parseRange function');
}
return {
start: 0,
end: fileSize - 1,
length: fileSize,
size: fileSize,
unit: 'bytes',
partial: false
};
};
/**
* @method FS.HTTP.Handlers.Get
* @public
* @returns {any} response
*
* HTTP GET request handler
*/
FS.HTTP.Handlers.Get = function httpGetHandler(ref) {
var self = this;
// Once we have the file, we can test allow/deny validators
// XXX: pass on the "share" query eg. ?share=342hkjh23ggj for shared url access?
FS.Utility.validateAction(ref.collection._validators['download'], ref.file, self.userId /*, self.query.shareId*/);
var storeName = ref.storeName;
// If no storeName was specified, use the first defined storeName
if (typeof storeName !== "string") {
// No store handed, we default to primary store
storeName = ref.collection.primaryStore.name;
}
// Get the storage reference
var storage = ref.collection.storesLookup[storeName];
if (!storage) {
throw new Meteor.Error(404, "Not Found", 'There is no store "' + storeName + '"');
}
// Get the file
var copyInfo = ref.file.copies[storeName];
if (!copyInfo) {
throw new Meteor.Error(404, "Not Found", 'This file was not stored in the ' + storeName + ' store');
}
// Set the content type for file
if (typeof copyInfo.type === "string") {
self.setContentType(copyInfo.type);
} else {
self.setContentType('application/octet-stream');
}
// Add 'Content-Disposition' header if requested a download/attachment URL
if (typeof ref.download !== "undefined") {
var filename = ref.filename || copyInfo.name;
self.addHeader('Content-Disposition', contentDisposition(filename));
} else {
self.addHeader('Content-Disposition', 'inline');
}
// Get the contents range from request
var range = requestRange(self.request, copyInfo.size);
// Some browsers cope better if the content-range header is
// still included even for the full file being returned.
self.addHeader('Content-Range', range.unit + ' ' + range.start + '-' + range.end + '/' + range.size);
// If a chunk/range was requested instead of the whole file, serve that'
if (range.partial) {
self.setStatusCode(206, 'Partial Content');
} else {
self.setStatusCode(200, 'OK');
}
// Add any other global custom headers and collection-specific custom headers
FS.Utility.each(getHeaders.concat(getHeadersByCollection[ref.collection.name] || []), function(header) {
self.addHeader(header[0], header[1]);
});
// Inform clients about length (or chunk length in case of ranges)
self.addHeader('Content-Length', range.length);
// Last modified header (updatedAt from file info)
self.addHeader('Last-Modified', copyInfo.updatedAt.toUTCString());
// Inform clients that we accept ranges for resumable chunked downloads
self.addHeader('Accept-Ranges', range.unit);
if (FS.debug) console.log('Read file "' + (ref.filename || copyInfo.name) + '" ' + range.unit + ' ' + range.start + '-' + range.end + '/' + range.size);
var readStream = storage.adapter.createReadStream(ref.file, {start: range.start, end: range.end});
readStream.on('error', function(err) {
// Send proper error message on get error
if (err.message && err.statusCode) {
self.Error(new Meteor.Error(err.statusCode, err.message));
} else {
self.Error(new Meteor.Error(503, 'Service unavailable'));
}
});
readStream.pipe(self.createWriteStream());
};
// File with unicode or other encodings filename can upload to server susscessfully,
// but when download, the HTTP header "Content-Disposition" cannot accept
// characters other than ASCII, the filename should be converted to binary or URI encoded.
// https://github.com/wekan/wekan/issues/784
const originalHandler = FS.HTTP.Handlers.Get;
FS.HTTP.Handlers.Get = function (ref) {
try {
var userAgent = (this.requestHeaders['user-agent']||'').toLowerCase();
if(userAgent.indexOf('msie') >= 0 || userAgent.indexOf('chrome') >= 0) {
ref.filename = encodeURIComponent(ref.filename);
} else if(userAgent.indexOf('firefox') >= 0) {
ref.filename = new Buffer(ref.filename).toString('binary');
} else {
/* safari*/
ref.filename = new Buffer(ref.filename).toString('binary');
}
} catch (ex){
ref.filename = ref.filename;
}
return originalHandler.call(this, ref);
};
/**
* @method FS.HTTP.Handlers.PutInsert
* @public
* @returns {Object} response object with _id property
*
* HTTP PUT file insert request handler
*/
FS.HTTP.Handlers.PutInsert = function httpPutInsertHandler(ref) {
var self = this;
var opts = FS.Utility.extend({}, self.query || {}, self.params || {});
FS.debug && console.log("HTTP PUT (insert) handler");
// Create the nice FS.File
var fileObj = new FS.File();
// Set its name
fileObj.name(opts.filename || null);
// Attach the readstream as the file's data
fileObj.attachData(self.createReadStream(), {type: self.requestHeaders['content-type'] || 'application/octet-stream'});
// Validate with insert allow/deny
FS.Utility.validateAction(ref.collection.files._validators['insert'], fileObj, self.userId);
// Insert file into collection, triggering readStream storage
ref.collection.insert(fileObj);
// Send response
self.setStatusCode(200);
// Return the new file id
return {_id: fileObj._id};
};
/**
* @method FS.HTTP.Handlers.PutUpdate
* @public
* @returns {Object} response object with _id and chunk properties
*
* HTTP PUT file update chunk request handler
*/
FS.HTTP.Handlers.PutUpdate = function httpPutUpdateHandler(ref) {
var self = this;
var opts = FS.Utility.extend({}, self.query || {}, self.params || {});
var chunk = parseInt(opts.chunk, 10);
if (isNaN(chunk)) chunk = 0;
FS.debug && console.log("HTTP PUT (update) handler received chunk: ", chunk);
// Validate with insert allow/deny; also mounts and retrieves the file
FS.Utility.validateAction(ref.collection.files._validators['insert'], ref.file, self.userId);
self.createReadStream().pipe( FS.TempStore.createWriteStream(ref.file, chunk) );
// Send response
self.setStatusCode(200);
return { _id: ref.file._id, chunk: chunk };
};

View file

@ -0,0 +1,362 @@
var path = Npm.require("path");
HTTP.publishFormats({
fileRecordFormat: function (input) {
// Set the method scope content type to json
this.setContentType('application/json');
if (FS.Utility.isArray(input)) {
return EJSON.stringify(FS.Utility.map(input, function (obj) {
return FS.Utility.cloneFileRecord(obj);
}));
} else {
return EJSON.stringify(FS.Utility.cloneFileRecord(input));
}
}
});
/**
* @method FS.HTTP.setHeadersForGet
* @public
* @param {Array} headers - List of headers, where each is a two-item array in which item 1 is the header name and item 2 is the header value.
* @param {Array|String} [collections] - Which collections the headers should be added for. Omit this argument to add the header for all collections.
* @returns {undefined}
*/
FS.HTTP.setHeadersForGet = function setHeadersForGet(headers, collections) {
if (typeof collections === "string") {
collections = [collections];
}
if (collections) {
FS.Utility.each(collections, function(collectionName) {
getHeadersByCollection[collectionName] = headers || [];
});
} else {
getHeaders = headers || [];
}
};
/**
* @method FS.HTTP.publish
* @public
* @param {FS.Collection} collection
* @param {Function} func - Publish function that returns a cursor.
* @returns {undefined}
*
* Publishes all documents returned by the cursor at a GET URL
* with the format baseUrl/record/collectionName. The publish
* function `this` is similar to normal `Meteor.publish`.
*/
FS.HTTP.publish = function fsHttpPublish(collection, func) {
var name = baseUrl + '/record/' + collection.name;
// Mount collection listing URL using http-publish package
HTTP.publish({
name: name,
defaultFormat: 'fileRecordFormat',
collection: collection,
collectionGet: true,
collectionPost: false,
documentGet: true,
documentPut: false,
documentDelete: false
}, func);
FS.debug && console.log("Registered HTTP method GET URLs:\n\n" + name + '\n' + name + '/:id\n');
};
/**
* @method FS.HTTP.unpublish
* @public
* @param {FS.Collection} collection
* @returns {undefined}
*
* Unpublishes a restpoint created by a call to `FS.HTTP.publish`
*/
FS.HTTP.unpublish = function fsHttpUnpublish(collection) {
// Mount collection listing URL using http-publish package
HTTP.unpublish(baseUrl + '/record/' + collection.name);
};
_existingMountPoints = {};
/**
* @method defaultSelectorFunction
* @private
* @returns { collection, file }
*
* This is the default selector function
*/
var defaultSelectorFunction = function() {
var self = this;
// Selector function
//
// This function will have to return the collection and the
// file. If file not found undefined is returned - if null is returned the
// search was not possible
var opts = FS.Utility.extend({}, self.query || {}, self.params || {});
// Get the collection name from the url
var collectionName = opts.collectionName;
// Get the id from the url
var id = opts.id;
// Get the collection
var collection = FS._collections[collectionName];
//if Mongo ObjectIds are used, then we need to use that in find statement
if(collection.options.idGeneration && collection.options.idGeneration === 'MONGO') {
// Get the file if possible else return null
var file = (id && collection)? collection.findOne({ _id: new Meteor.Collection.ObjectID(id)}): null;
} else {
var file = (id && collection)? collection.findOne({ _id: id }): null;
}
// Return the collection and the file
return {
collection: collection,
file: file,
storeName: opts.store,
download: opts.download,
filename: opts.filename
};
};
/*
* @method FS.HTTP.mount
* @public
* @param {array of string} mountPoints mount points to map rest functinality on
* @param {function} selector_f [selector] function returns `{ collection, file }` for mount points to work with
*
*/
FS.HTTP.mount = function(mountPoints, selector_f) {
// We take mount points as an array and we get a selector function
var selectorFunction = selector_f || defaultSelectorFunction;
var accessPoint = {
'stream': true,
'auth': expirationAuth,
'post': function(data) {
// Use the selector for finding the collection and file reference
var ref = selectorFunction.call(this);
// We dont support post - this would be normal insert eg. of filerecord?
throw new Meteor.Error(501, "Not implemented", "Post is not supported");
},
'put': function(data) {
// Use the selector for finding the collection and file reference
var ref = selectorFunction.call(this);
// Make sure we have a collection reference
if (!ref.collection)
throw new Meteor.Error(404, "Not Found", "No collection found");
// Make sure we have a file reference
if (ref.file === null) {
// No id supplied so we will create a new FS.File instance and
// insert the supplied data.
return FS.HTTP.Handlers.PutInsert.apply(this, [ref]);
} else {
if (ref.file) {
return FS.HTTP.Handlers.PutUpdate.apply(this, [ref]);
} else {
throw new Meteor.Error(404, "Not Found", 'No file found');
}
}
},
'get': function(data) {
// Use the selector for finding the collection and file reference
var ref = selectorFunction.call(this);
// Make sure we have a collection reference
if (!ref.collection)
throw new Meteor.Error(404, "Not Found", "No collection found");
// Make sure we have a file reference
if (ref.file === null) {
// No id supplied so we will return the published list of files ala
// http.publish in json format
return FS.HTTP.Handlers.GetList.apply(this, [ref]);
} else {
if (ref.file) {
return FS.HTTP.Handlers.Get.apply(this, [ref]);
} else {
throw new Meteor.Error(404, "Not Found", 'No file found');
}
}
},
'delete': function(data) {
// Use the selector for finding the collection and file reference
var ref = selectorFunction.call(this);
// Make sure we have a collection reference
if (!ref.collection)
throw new Meteor.Error(404, "Not Found", "No collection found");
// Make sure we have a file reference
if (ref.file) {
return FS.HTTP.Handlers.Del.apply(this, [ref]);
} else {
throw new Meteor.Error(404, "Not Found", 'No file found');
}
}
};
var accessPoints = {};
// Add debug message
FS.debug && console.log('Registered HTTP method URLs:');
FS.Utility.each(mountPoints, function(mountPoint) {
// Couple mountpoint and accesspoint
accessPoints[mountPoint] = accessPoint;
// Remember our mountpoints
_existingMountPoints[mountPoint] = mountPoint;
// Add debug message
FS.debug && console.log(mountPoint);
});
// XXX: HTTP:methods should unmount existing mounts in case of overwriting?
HTTP.methods(accessPoints);
};
/**
* @method FS.HTTP.unmount
* @public
* @param {string | array of string} [mountPoints] Optional, if not specified all mountpoints are unmounted
*
*/
FS.HTTP.unmount = function(mountPoints) {
// The mountPoints is optional, can be string or array if undefined then
// _existingMountPoints will be used
var unmountList;
// Container for the mount points to unmount
var unmountPoints = {};
if (typeof mountPoints === 'undefined') {
// Use existing mount points - unmount all
unmountList = _existingMountPoints;
} else if (mountPoints === ''+mountPoints) {
// Got a string
unmountList = [mountPoints];
} else if (mountPoints.length) {
// Got an array
unmountList = mountPoints;
}
// If we have a list to unmount
if (unmountList) {
// Iterate over each item
FS.Utility.each(unmountList, function(mountPoint) {
// Check _existingMountPoints to make sure the mount point exists in our
// context / was created by the FS.HTTP.mount
if (_existingMountPoints[mountPoint]) {
// Mark as unmount
unmountPoints[mountPoint] = false;
// Release
delete _existingMountPoints[mountPoint];
}
});
FS.debug && console.log('FS.HTTP.unmount:');
FS.debug && console.log(unmountPoints);
// Complete unmount
HTTP.methods(unmountPoints);
}
};
// ### FS.Collection maps on HTTP pr. default on the following restpoints:
// *
// baseUrl + '/files/:collectionName/:id/:filename',
// baseUrl + '/files/:collectionName/:id',
// baseUrl + '/files/:collectionName'
//
// Change/ replace the existing mount point by:
// ```js
// // unmount all existing
// FS.HTTP.unmount();
// // Create new mount point
// FS.HTTP.mount([
// '/cfs/files/:collectionName/:id/:filename',
// '/cfs/files/:collectionName/:id',
// '/cfs/files/:collectionName'
// ]);
// ```
//
mountUrls = function mountUrls() {
// We unmount first in case we are calling this a second time
FS.HTTP.unmount();
FS.HTTP.mount([
baseUrl + '/files/:collectionName/:id/:filename',
baseUrl + '/files/:collectionName/:id',
baseUrl + '/files/:collectionName'
]);
};
// Returns the userId from URL token
var expirationAuth = function expirationAuth() {
var self = this;
// Read the token from '/hello?token=base64'
var encodedToken = self.query.token;
FS.debug && console.log("token: "+encodedToken);
if (!encodedToken || !Meteor.users) return false;
// Check the userToken before adding it to the db query
// Set the this.userId
var tokenString = FS.Utility.atob(encodedToken);
var tokenObject;
try {
tokenObject = JSON.parse(tokenString);
} catch(err) {
throw new Meteor.Error(400, 'Bad Request');
}
// XXX: Do some check here of the object
var userToken = tokenObject.authToken;
if (userToken !== ''+userToken) {
throw new Meteor.Error(400, 'Bad Request');
}
// If we have an expiration token we should check that it's still valid
if (tokenObject.expiration != null) {
// check if its too old
var now = Date.now();
if (tokenObject.expiration < now) {
FS.debug && console.log('Expired token: ' + tokenObject.expiration + ' is less than ' + now);
throw new Meteor.Error(500, 'Expired token');
}
}
// We are not on a secure line - so we have to look up the user...
var user = Meteor.users.findOne({
$or: [
{'services.resume.loginTokens.hashedToken': Accounts._hashLoginToken(userToken)},
{'services.resume.loginTokens.token': userToken}
]
});
// Set the userId in the scope
return user && user._id;
};
HTTP.methods(
{'/cfs/servertime': {
get: function(data) {
return Date.now().toString();
}
}
});
// Unify client / server api
FS.HTTP.now = function() {
return Date.now();
};
// Start up the basic mount points
Meteor.startup(function () {
mountUrls();
});

View file

@ -0,0 +1,271 @@
## wekan-cfs-access-point Public API ##
CollectionFS, add ddp and http accesspoint capability
_API documentation automatically generated by [docmeteor](https://github.com/raix/docmeteor)._
-
### <a name="FS.HTTP.setBaseUrl"></a>*FSHTTP*.setBaseUrl(newBaseUrl)&nbsp;&nbsp;<sub><i>Anywhere</i></sub> ###
*This method __setBaseUrl__ is defined in `FS.HTTP`*
__Arguments__
* __newBaseUrl__ *{String}*
Change the base URL for the HTTP GET and DELETE endpoints.
__Returns__ *{undefined}*
> ```FS.HTTP.setBaseUrl = function setBaseUrl(newBaseUrl) { ...``` [access-point-common.js:29](access-point-common.js#L29)
-
### <a name="FS.File.prototype.url"></a>*fsFile*.url([options])&nbsp;&nbsp;<sub><i>Anywhere</i></sub> ###
*This method __url__ is defined in `prototype` of `FS.File`*
__Arguments__
* __options__ *{Object}* (Optional)
* __store__ *{String}* (Optional)
Name of the store to get from. If not defined, the first store defined in `options.stores` for the collection on the client is used.
* __auth__ *{Boolean}* (Optional, Default = null)
Add authentication token to the URL query string? By default, a token for the current logged in user is added on the client. Set this to `false` to omit the token. Set this to a string to provide your own token. Set this to a number to specify an expiration time for the token in seconds.
* __download__ *{Boolean}* (Optional, Default = false)
Should headers be set to force a download? Typically this means that clicking the link with this URL will download the file to the user's Downloads folder instead of displaying the file in the browser.
* __brokenIsFine__ *{Boolean}* (Optional, Default = false)
Return the URL even if we know it's currently a broken link because the file hasn't been saved in the requested store yet.
* __metadata__ *{Boolean}* (Optional, Default = false)
Return the URL for the file metadata access point rather than the file itself.
* __uploading__ *{String}* (Optional, Default = null)
A URL to return while the file is being uploaded.
* __storing__ *{String}* (Optional, Default = null)
A URL to return while the file is being stored.
* __filename__ *{String}* (Optional, Default = null)
Override the filename that should appear at the end of the URL. By default it is the name of the file in the requested store.
Returns the HTTP URL for getting the file or its metadata.
> ```FS.File.prototype.url = function(options) { ...``` [access-point-common.js:72](access-point-common.js#L72)
-
### <a name="FS.HTTP.Handlers.Del"></a>*FSHTTPHandlers*.Del()&nbsp;&nbsp;<sub><i>Server</i></sub> ###
*This method __Del__ is defined in `FS.HTTP.Handlers`*
__Returns__ *{any}*
response
HTTP DEL request handler
> ```FS.HTTP.Handlers.Del = function httpDelHandler(ref) { ...``` [access-point-handlers.js:13](access-point-handlers.js#L13)
-
### <a name="FS.HTTP.Handlers.GetList"></a>*FSHTTPHandlers*.GetList()&nbsp;&nbsp;<sub><i>Server</i></sub> ###
*This method __GetList__ is defined in `FS.HTTP.Handlers`*
__Returns__ *{Object}*
response
HTTP GET file list request handler
> ```FS.HTTP.Handlers.GetList = function httpGetListHandler() { ...``` [access-point-handlers.js:41](access-point-handlers.js#L41)
-
### <a name="FS.HTTP.Handlers.Get"></a>*FSHTTPHandlers*.Get()&nbsp;&nbsp;<sub><i>Server</i></sub> ###
*This method __Get__ is defined in `FS.HTTP.Handlers`*
__Returns__ *{any}*
response
HTTP GET request handler
> ```FS.HTTP.Handlers.Get = function httpGetHandler(ref) { ...``` [access-point-handlers.js:135](access-point-handlers.js#L135)
-
### <a name="FS.HTTP.Handlers.PutInsert"></a>*FSHTTPHandlers*.PutInsert()&nbsp;&nbsp;<sub><i>Server</i></sub> ###
*This method __PutInsert__ is defined in `FS.HTTP.Handlers`*
__Returns__ *{Object}*
response object with _id property
HTTP PUT file insert request handler
> ```FS.HTTP.Handlers.PutInsert = function httpPutInsertHandler(ref) { ...``` [access-point-handlers.js:229](access-point-handlers.js#L229)
-
### <a name="FS.HTTP.Handlers.PutUpdate"></a>*FSHTTPHandlers*.PutUpdate()&nbsp;&nbsp;<sub><i>Server</i></sub> ###
*This method __PutUpdate__ is defined in `FS.HTTP.Handlers`*
__Returns__ *{Object}*
response object with _id and chunk properties
HTTP PUT file update chunk request handler
> ```FS.HTTP.Handlers.PutUpdate = function httpPutUpdateHandler(ref) { ...``` [access-point-handlers.js:264](access-point-handlers.js#L264)
-
### <a name="FS.HTTP.setHeadersForGet"></a>*FSHTTP*.setHeadersForGet(headers, [collections])&nbsp;&nbsp;<sub><i>Server</i></sub> ###
*This method __setHeadersForGet__ is defined in `FS.HTTP`*
__Arguments__
* __headers__ *{Array}*
List of headers, where each is a two-item array in which item 1 is the header name and item 2 is the header value.
* __collections__ *{Array|String}* (Optional)
Which collections the headers should be added for. Omit this argument to add the header for all collections.
__Returns__ *{undefined}*
> ```FS.HTTP.setHeadersForGet = function setHeadersForGet(headers, collections) { ...``` [access-point-server.js:24](access-point-server.js#L24)
-
### <a name="FS.HTTP.publish"></a>*FSHTTP*.publish(collection, func)&nbsp;&nbsp;<sub><i>Server</i></sub> ###
*This method __publish__ is defined in `FS.HTTP`*
__Arguments__
* __collection__ *{[FS.Collection](#FS.Collection)}*
* __func__ *{Function}*
Publish function that returns a cursor.
__Returns__ *{undefined}*
Publishes all documents returned by the cursor at a GET URL
with the format baseUrl/record/collectionName. The publish
function `this` is similar to normal `Meteor.publish`.
> ```FS.HTTP.publish = function fsHttpPublish(collection, func) { ...``` [access-point-server.js:48](access-point-server.js#L48)
-
### <a name="FS.HTTP.unpublish"></a>*FSHTTP*.unpublish(collection)&nbsp;&nbsp;<sub><i>Server</i></sub> ###
*This method __unpublish__ is defined in `FS.HTTP`*
__Arguments__
* __collection__ *{[FS.Collection](#FS.Collection)}*
__Returns__ *{undefined}*
Unpublishes a restpoint created by a call to `FS.HTTP.publish`
> ```FS.HTTP.unpublish = function fsHttpUnpublish(collection) { ...``` [access-point-server.js:73](access-point-server.js#L73)
-
### <a name="FS.HTTP.mount"></a>*FSHTTP*.mount(mountPoints, selector_f)&nbsp;&nbsp;<sub><i>Server</i></sub> ###
*This method __mount__ is defined in `FS.HTTP`*
__Arguments__
* __mountPoints__ *{[array of string](#array of string)}*
mount points to map rest functinality on
* __selector_f__ *{function}*
[selector] function returns `{ collection, file }` for mount points to work with
> ```FS.HTTP.mount = function(mountPoints, selector_f) { ...``` [access-point-server.js:125](access-point-server.js#L125)
-
### <a name="FS.HTTP.unmount"></a>*FSHTTP*.unmount([mountPoints])&nbsp;&nbsp;<sub><i>Server</i></sub> ###
*This method __unmount__ is defined in `FS.HTTP`*
__Arguments__
* __mountPoints__ *{[string ](#string )|[ array of string](# array of string)}* (Optional)
Optional, if not specified all mountpoints are unmounted
> ```FS.HTTP.unmount = function(mountPoints) { ...``` [access-point-server.js:223](access-point-server.js#L223)
-
### FS.Collection maps on HTTP pr. default on the following restpoints:
*
baseUrl + '/files/:collectionName/:id/:filename',
baseUrl + '/files/:collectionName/:id',
baseUrl + '/files/:collectionName'
Change/ replace the existing mount point by:
```js
unmount all existing
FS.HTTP.unmount();
Create new mount point
FS.HTTP.mount([
'/cfs/files/:collectionName/:id/:filename',
'/cfs/files/:collectionName/:id',
'/cfs/files/:collectionName'
]);
```

View file

@ -0,0 +1,332 @@
## Public and Private API ##
_API documentation automatically generated by [docmeteor](https://github.com/raix/docmeteor)._
***
__File: ["access-point-common.js"](access-point-common.js) Where: {server|client}__
***
### <a name="FS.HTTP.setBaseUrl"></a>*FSHTTP*.setBaseUrl(newBaseUrl)&nbsp;&nbsp;<sub><i>Anywhere</i></sub> ###
*This method __setBaseUrl__ is defined in `FS.HTTP`*
__Arguments__
* __newBaseUrl__ *{String}*
Change the base URL for the HTTP GET and DELETE endpoints.
__Returns__ *{undefined}*
> ```FS.HTTP.setBaseUrl = function setBaseUrl(newBaseUrl) { ...``` [access-point-common.js:29](access-point-common.js#L29)
-
### <a name="FS.File.prototype.url"></a>*fsFile*.url([options])&nbsp;&nbsp;<sub><i>Anywhere</i></sub> ###
*This method __url__ is defined in `prototype` of `FS.File`*
__Arguments__
* __options__ *{Object}* (Optional)
* __store__ *{String}* (Optional)
Name of the store to get from. If not defined, the first store defined in `options.stores` for the collection on the client is used.
* __auth__ *{Boolean}* (Optional, Default = null)
Add authentication token to the URL query string? By default, a token for the current logged in user is added on the client. Set this to `false` to omit the token. Set this to a string to provide your own token. Set this to a number to specify an expiration time for the token in seconds.
* __download__ *{Boolean}* (Optional, Default = false)
Should headers be set to force a download? Typically this means that clicking the link with this URL will download the file to the user's Downloads folder instead of displaying the file in the browser.
* __brokenIsFine__ *{Boolean}* (Optional, Default = false)
Return the URL even if we know it's currently a broken link because the file hasn't been saved in the requested store yet.
* __metadata__ *{Boolean}* (Optional, Default = false)
Return the URL for the file metadata access point rather than the file itself.
* __uploading__ *{String}* (Optional, Default = null)
A URL to return while the file is being uploaded.
* __storing__ *{String}* (Optional, Default = null)
A URL to return while the file is being stored.
* __filename__ *{String}* (Optional, Default = null)
Override the filename that should appear at the end of the URL. By default it is the name of the file in the requested store.
Returns the HTTP URL for getting the file or its metadata.
> ```FS.File.prototype.url = function(options) { ...``` [access-point-common.js:72](access-point-common.js#L72)
***
__File: ["access-point-handlers.js"](access-point-handlers.js) Where: {server}__
***
### <a name="FS.HTTP.Handlers.Del"></a>*FSHTTPHandlers*.Del()&nbsp;&nbsp;<sub><i>Server</i></sub> ###
*This method __Del__ is defined in `FS.HTTP.Handlers`*
__Returns__ *{any}*
response
HTTP DEL request handler
> ```FS.HTTP.Handlers.Del = function httpDelHandler(ref) { ...``` [access-point-handlers.js:13](access-point-handlers.js#L13)
-
### <a name="self.setStatusCode"></a>*self*.setStatusCode {any}&nbsp;&nbsp;<sub><i>Server</i></sub> ###
```
From the DELETE spec:
A successful response SHOULD be 200 (OK) if the response includes an
entity describing the status, 202 (Accepted) if the action has not
yet been enacted, or 204 (No Content) if the action has been enacted
but the response does not include an entity.
```
*This property __setStatusCode__ is defined in `self`*
> ```self.setStatusCode(200);``` [access-point-handlers.js:27](access-point-handlers.js#L27)
-
### <a name="FS.HTTP.Handlers.GetList"></a>*FSHTTPHandlers*.GetList()&nbsp;&nbsp;<sub><i>Server</i></sub> ###
*This method __GetList__ is defined in `FS.HTTP.Handlers`*
__Returns__ *{Object}*
response
HTTP GET file list request handler
> ```FS.HTTP.Handlers.GetList = function httpGetListHandler() { ...``` [access-point-handlers.js:41](access-point-handlers.js#L41)
-
### <a name="requestRange"></a>requestRange {any}&nbsp;&nbsp;<sub><i>Server</i></sub> ###
```
requestRange will parse the range set in request header - if not possible it
will throw fitting errors and autofill range for both partial and full ranges
throws error or returns the object:
{
start
end
length
unit
partial
}
```
*This property is private*
> ```var requestRange = function(req, fileSize) { ...``` [access-point-handlers.js:60](access-point-handlers.js#L60)
-
### <a name="FS.HTTP.Handlers.Get"></a>*FSHTTPHandlers*.Get()&nbsp;&nbsp;<sub><i>Server</i></sub> ###
*This method __Get__ is defined in `FS.HTTP.Handlers`*
__Returns__ *{any}*
response
HTTP GET request handler
> ```FS.HTTP.Handlers.Get = function httpGetHandler(ref) { ...``` [access-point-handlers.js:135](access-point-handlers.js#L135)
-
### <a name="FS.HTTP.Handlers.PutInsert"></a>*FSHTTPHandlers*.PutInsert()&nbsp;&nbsp;<sub><i>Server</i></sub> ###
*This method __PutInsert__ is defined in `FS.HTTP.Handlers`*
__Returns__ *{Object}*
response object with _id property
HTTP PUT file insert request handler
> ```FS.HTTP.Handlers.PutInsert = function httpPutInsertHandler(ref) { ...``` [access-point-handlers.js:229](access-point-handlers.js#L229)
-
### <a name="FS.HTTP.Handlers.PutUpdate"></a>*FSHTTPHandlers*.PutUpdate()&nbsp;&nbsp;<sub><i>Server</i></sub> ###
*This method __PutUpdate__ is defined in `FS.HTTP.Handlers`*
__Returns__ *{Object}*
response object with _id and chunk properties
HTTP PUT file update chunk request handler
> ```FS.HTTP.Handlers.PutUpdate = function httpPutUpdateHandler(ref) { ...``` [access-point-handlers.js:264](access-point-handlers.js#L264)
***
__File: ["access-point-server.js"](access-point-server.js) Where: {server}__
***
### <a name="FS.HTTP.setHeadersForGet"></a>*FSHTTP*.setHeadersForGet(headers, [collections])&nbsp;&nbsp;<sub><i>Server</i></sub> ###
*This method __setHeadersForGet__ is defined in `FS.HTTP`*
__Arguments__
* __headers__ *{Array}*
List of headers, where each is a two-item array in which item 1 is the header name and item 2 is the header value.
* __collections__ *{Array|String}* (Optional)
Which collections the headers should be added for. Omit this argument to add the header for all collections.
__Returns__ *{undefined}*
> ```FS.HTTP.setHeadersForGet = function setHeadersForGet(headers, collections) { ...``` [access-point-server.js:24](access-point-server.js#L24)
-
### <a name="FS.HTTP.publish"></a>*FSHTTP*.publish(collection, func)&nbsp;&nbsp;<sub><i>Server</i></sub> ###
*This method __publish__ is defined in `FS.HTTP`*
__Arguments__
* __collection__ *{[FS.Collection](#FS.Collection)}*
* __func__ *{Function}*
Publish function that returns a cursor.
__Returns__ *{undefined}*
Publishes all documents returned by the cursor at a GET URL
with the format baseUrl/record/collectionName. The publish
function `this` is similar to normal `Meteor.publish`.
> ```FS.HTTP.publish = function fsHttpPublish(collection, func) { ...``` [access-point-server.js:48](access-point-server.js#L48)
-
### <a name="FS.HTTP.unpublish"></a>*FSHTTP*.unpublish(collection)&nbsp;&nbsp;<sub><i>Server</i></sub> ###
*This method __unpublish__ is defined in `FS.HTTP`*
__Arguments__
* __collection__ *{[FS.Collection](#FS.Collection)}*
__Returns__ *{undefined}*
Unpublishes a restpoint created by a call to `FS.HTTP.publish`
> ```FS.HTTP.unpublish = function fsHttpUnpublish(collection) { ...``` [access-point-server.js:73](access-point-server.js#L73)
-
### <a name="defaultSelectorFunction"></a>defaultSelectorFunction()&nbsp;&nbsp;<sub><i>Server</i></sub> ###
*This method is private*
__Returns__ *{ collection, file }*
This is the default selector function
> ```var defaultSelectorFunction = function() { ...``` [access-point-server.js:87](access-point-server.js#L87)
-
### <a name="FS.HTTP.mount"></a>*FSHTTP*.mount(mountPoints, selector_f)&nbsp;&nbsp;<sub><i>Server</i></sub> ###
*This method __mount__ is defined in `FS.HTTP`*
__Arguments__
* __mountPoints__ *{[array of string](#array of string)}*
mount points to map rest functinality on
* __selector_f__ *{function}*
[selector] function returns `{ collection, file }` for mount points to work with
> ```FS.HTTP.mount = function(mountPoints, selector_f) { ...``` [access-point-server.js:125](access-point-server.js#L125)
-
### <a name="FS.HTTP.unmount"></a>*FSHTTP*.unmount([mountPoints])&nbsp;&nbsp;<sub><i>Server</i></sub> ###
*This method __unmount__ is defined in `FS.HTTP`*
__Arguments__
* __mountPoints__ *{[string ](#string )|[ array of string](# array of string)}* (Optional)
Optional, if not specified all mountpoints are unmounted
> ```FS.HTTP.unmount = function(mountPoints) { ...``` [access-point-server.js:223](access-point-server.js#L223)
-
### FS.Collection maps on HTTP pr. default on the following restpoints:
*
baseUrl + '/files/:collectionName/:id/:filename',
baseUrl + '/files/:collectionName/:id',
baseUrl + '/files/:collectionName'
Change/ replace the existing mount point by:
```js
unmount all existing
FS.HTTP.unmount();
Create new mount point
FS.HTTP.mount([
'/cfs/files/:collectionName/:id/:filename',
'/cfs/files/:collectionName/:id',
'/cfs/files/:collectionName'
]);
```

View file

@ -0,0 +1,65 @@
Package.describe({
name: 'wekan-cfs-access-point',
version: '0.1.50',
summary: 'CollectionFS, add ddp and http accesspoint capability',
git: 'https://github.com/zcfs/Meteor-cfs-access-point.git'
});
Npm.depends({
"content-disposition": "0.5.0"
});
Package.onUse(function(api) {
api.versionsFrom('1.0');
// This imply is needed for tests, and is technically probably correct anyway.
api.imply([
'wekan-cfs-base-package'
]);
api.use([
//CFS packages
'wekan-cfs-base-package@0.0.30',
'wekan-cfs-file@0.1.16',
//Core packages
'check',
'ejson',
//Other packages
'wekan-cfs-http-methods@0.0.29',
'wekan-cfs-http-publish@0.0.13'
]);
api.addFiles([
'access-point-common.js',
'access-point-handlers.js',
'access-point-server.js'
], 'server');
api.addFiles([
'access-point-common.js',
'access-point-client.js'
], 'client');
});
Package.onTest(function (api) {
api.versionsFrom('1.0');
api.use([
//CFS packages
'wekan-cfs-access-point',
'wekan-cfs-standard-packages@0.0.2',
'wekan-cfs-gridfs@0.0.0',
//Core packages
'test-helpers',
'http',
'tinytest',
'underscore',
'ejson',
'ordered-dict',
'random',
'deps'
]);
api.addFiles('tests/client-tests.js', 'client');
api.addFiles('tests/server-tests.js', 'server');
});

View file

@ -0,0 +1,125 @@
function equals(a, b) {
return !!(EJSON.stringify(a) === EJSON.stringify(b));
}
Tinytest.add('cfs-access-point - client - test environment', function(test) {
test.isTrue(typeof FS.Collection !== 'undefined', 'test environment not initialized FS.Collection');
test.isTrue(typeof FS.HTTP !== 'undefined', 'test environment not initialized FS.HTTP');
});
Images = new FS.Collection('images', {
stores: [
new FS.Store.GridFS('gridList')
]
});
Meteor.subscribe("img");
var id;
Tinytest.addAsync('cfs-access-point - client - addTestImage', function(test, onComplete) {
Meteor.call('addTestImage', function(err, result) {
id = result;
test.equal(typeof id, "string", "Test image was not inserted properly");
//Don't continue until the data has been stored
Deps.autorun(function (c) {
var img = Images.findOne(id);
if (img && img.hasCopy('gridList')) {
onComplete();
c.stop();
}
});
});
});
Tinytest.addAsync('cfs-access-point - client - GET list of files in collection', function(test, onComplete) {
HTTP.get(Meteor.absoluteUrl('cfs/record/images'), function(err, result) {
// Test the length of array result
var len = result.data && result.data.length;
test.isTrue(!!len, 'Result was empty');
// Get the object
var obj = result.data && result.data[0] || {};
test.equal(obj._id, id, 'Didn\'t get the expected result');
onComplete();
});
});
Tinytest.addAsync('cfs-access-point - client - GET filerecord', function(test, onComplete) {
HTTP.get(Meteor.absoluteUrl('cfs/record/images/' + id), function(err, result) {
// Get the object
var obj = result.data;
test.equal(typeof obj, "object", "Expected object data");
test.equal(obj._id, id, 'Didn\'t get the expected result');
onComplete();
});
});
Tinytest.addAsync('cfs-access-point - client - GET file itself', function(test, onComplete) {
HTTP.get(Meteor.absoluteUrl('cfs/files/images/' + id), function(err, result) {
test.isTrue(!!result.content, "Expected content in response");
console.log(result);
test.equal(result.statusCode, 200, "Expected 200 OK response");
onComplete();
});
});
Tinytest.addAsync('cfs-access-point - client - PUT new file data (update)', function(test, onComplete) {
// TODO
// HTTP.put(Meteor.absoluteUrl('cfs/files/images/' + id), function(err, result) {
// test.equal(result.statusCode, 200, "Expected 200 OK response");
onComplete();
// });
});
Tinytest.addAsync('cfs-access-point - client - PUT insert a new file', function(test, onComplete) {
// TODO
// HTTP.put(Meteor.absoluteUrl('cfs/files/images'), function(err, result) {
// test.equal(result.statusCode, 200, "Expected 200 OK response");
onComplete();
// });
});
Tinytest.addAsync('cfs-access-point - client - DELETE filerecord and data', function(test, onComplete) {
HTTP.del(Meteor.absoluteUrl('cfs/files/images/' + id), function(err, result) {
test.equal(result.statusCode, 200, "Expected 200 OK response");
// Make sure it's gone
HTTP.get(Meteor.absoluteUrl('cfs/record/images/' + id), function(err, result) {
test.isTrue(!!err, 'Expected 404 error');
test.equal(result.statusCode, 404, "Expected 404 response");
onComplete();
});
});
});
//TODO test FS.File.prototype.url method with various options
//Test API:
//test.isFalse(v, msg)
//test.isTrue(v, msg)
//test.equalactual, expected, message, not
//test.length(obj, len)
//test.include(s, v)
//test.isNaN(v, msg)
//test.isUndefined(v, msg)
//test.isNotNull
//test.isNull
//test.throws(func)
//test.instanceOf(obj, klass)
//test.notEqual(actual, expected, message)
//test.runId()
//test.exception(exception)
//test.expect_fail()
//test.ok(doc)
//test.fail(doc)
//test.equal(a, b, msg)

View file

@ -0,0 +1,68 @@
function equals(a, b) {
return !!(EJSON.stringify(a) === EJSON.stringify(b));
}
FS.debug = true;
Tinytest.add('cfs-access-point - server - test environment', function(test) {
test.isTrue(typeof FS.Collection !== 'undefined', 'test environment not initialized FS.Collection');
test.isTrue(typeof FS.HTTP !== 'undefined', 'test environment not initialized FS.HTTP');
});
Images = new FS.Collection('images', {
stores: [
new FS.Store.GridFS('gridList')
]
});
Images.allow({
insert: function() {
return true;
},
update: function() {
return true;
},
remove: function() {
return true;
},
download: function() {
return true;
}
});
Meteor.publish("img", function () {
return Images.find();
});
FS.HTTP.publish(Images, function () {
return Images.find();
});
Meteor.methods({
addTestImage: function() {
Images.remove({});
var url = "http://cdn.morguefile.com/imageData/public/files/b/bboomerindenial/preview/fldr_2009_04_01/file3301238617907.jpg";
var fsFile = Images.insert(url);
return fsFile._id;
}
});
//Test API:
//test.isFalse(v, msg)
//test.isTrue(v, msg)
//test.equalactual, expected, message, not
//test.length(obj, len)
//test.include(s, v)
//test.isNaN(v, msg)
//test.isUndefined(v, msg)
//test.isNotNull
//test.isNull
//test.throws(func)
//test.instanceOf(obj, klass)
//test.notEqual(actual, expected, message)
//test.runId()
//test.exception(exception)
//test.expect_fail()
//test.ok(doc)
//test.fail(doc)
//test.equal(a, b, msg)