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

@ -17,7 +17,7 @@ es5-shim@4.8.0
# Collections
aldeed:collection2
cfs:standard-packages
wekan-cfs-standard-packages
cottz:publish-relations
dburles:collection-helpers
idmontie:migrations
@ -75,7 +75,7 @@ horka:swipebox
dynamic-import@0.6.0
accounts-password@1.7.0
cfs:gridfs
wekan-cfs-gridfs
rzymek:fullcalendar
momentjs:moment@2.22.2
browser-policy-framing@1.1.0
@ -91,7 +91,7 @@ meteorhacks:aggregate@1.3.0
wekan-markdown
konecty:mongo-counter
percolate:synced-cron
cfs:filesystem
wekan-cfs-filesystem
tmeasday:check-npm-versions
steffo:meteor-accounts-saml
rajit:bootstrap3-datepicker-fi

View file

@ -23,24 +23,7 @@ browser-policy-framing@1.1.0
caching-compiler@1.2.2
caching-html-compiler@1.2.0
callback-hook@1.3.0
cfs:access-point@0.1.49
cfs:base-package@0.0.30
cfs:collection@0.5.5
cfs:collection-filters@0.2.4
cfs:data-man@0.0.6
cfs:file@0.1.17
cfs:filesystem@0.1.2
cfs:gridfs@0.0.34
cfs:http-methods@0.0.32
cfs:http-publish@0.0.13
cfs:power-queue@0.9.11
cfs:reactive-list@0.0.9
cfs:reactive-property@0.0.4
cfs:standard-packages@0.5.10
cfs:storage-adapter@0.2.4
cfs:tempstore@0.1.6
cfs:upload-http@0.0.20
cfs:worker@0.1.5
check@1.3.1
chuangbo:cookie@1.1.0
coagmano:stylus@1.1.0
@ -238,6 +221,24 @@ webapp@1.10.1
webapp-hashing@1.1.0
wekan-accounts-cas@0.1.0
wekan-accounts-oidc@1.0.10
wekan-cfs-access-point@0.1.50
wekan-cfs-base-package@0.0.30
wekan-cfs-collection@0.5.5
wekan-cfs-collection-filters@0.2.4
wekan-cfs-data-man@0.0.6
wekan-cfs-file@0.1.17
wekan-cfs-filesystem@0.1.2
wekan-cfs-gridfs@0.0.34
wekan-cfs-http-methods@0.0.32
wekan-cfs-http-publish@0.0.13
wekan-cfs-power-queue@0.9.11
wekan-cfs-reactive-list@0.0.9
wekan-cfs-reactive-property@0.0.4
wekan-cfs-standard-packages@0.5.10
wekan-cfs-storage-adapter@0.2.4
wekan-cfs-tempstore@0.1.6
wekan-cfs-upload-http@0.0.21
wekan-cfs-worker@0.1.5
wekan-ldap@0.0.2
wekan-markdown@1.0.9
wekan-oidc@1.0.12

View file

@ -241,7 +241,7 @@ Template.editor.onRendered(() => {
//the function is called before the text is really pasted.
updatePastedText(thisNote);
}, 10);
}
},
},
dialogsInBody: true,
spellCheck: true,

View file

@ -23,21 +23,30 @@ class DateFilter {
// past builds a filter for all dates before now
past() {
if (this._filterState == 'past') { this.reset(); return; }
if (this._filterState == 'past') {
this.reset();
return;
}
this._filter = { $lte: moment().toDate() };
this._updateState('past');
}
// today is a convenience method for calling relativeDay with 0
today() {
if (this._filterState == 'today') { this.reset(); return; }
if (this._filterState == 'today') {
this.reset();
return;
}
this.relativeDay(0);
this._updateState('today');
}
// tomorrow is a convenience method for calling relativeDay with 1
tomorrow() {
if (this._filterState == 'tomorrow') { this.reset(); return; }
if (this._filterState == 'tomorrow') {
this.reset();
return;
}
this.relativeDay(1);
this._updateState('tomorrow');
}
@ -50,10 +59,18 @@ class DateFilter {
// relativeDay builds a filter starting from now and including all
// days up to today +/- offset.
relativeDay(offset) {
if (this._filterState == 'day') { this.reset(); return; }
if (this._filterState == 'day') {
this.reset();
return;
}
var startDay = moment().startOf('day').toDate(),
endDay = moment().endOf('day').add(offset, 'day').toDate();
var startDay = moment()
.startOf('day')
.toDate(),
endDay = moment()
.endOf('day')
.add(offset, 'day')
.toDate();
if (offset >= 0) {
this._filter = { $gte: startDay, $lte: endDay };
@ -68,7 +85,10 @@ class DateFilter {
// weeks up to today +/- offset. This considers the user's preferred
// start of week day (as defined by Meteor).
relativeWeek(offset) {
if (this._filterState == 'week') { this.reset(); return; }
if (this._filterState == 'week') {
this.reset();
return;
}
// getStartDayOfWeek returns the offset from Sunday of the user's
// preferred starting day of the week. This date should be added
@ -78,8 +98,14 @@ class DateFilter {
const weekStartDay = currentUser ? currentUser.getStartDayOfWeek() : 1;
// Moments are mutable so they must be cloned before modification
var thisWeekStart = moment().startOf('day').startOf('week').add(weekStartDay, 'days');
var thisWeekEnd = thisWeekStart.clone().add(offset, 'week').endOf('day');
var thisWeekStart = moment()
.startOf('day')
.startOf('week')
.add(weekStartDay, 'days');
var thisWeekEnd = thisWeekStart
.clone()
.add(offset, 'week')
.endOf('day');
var startDate = thisWeekStart.toDate();
var endDate = thisWeekEnd.toDate();
@ -94,7 +120,10 @@ class DateFilter {
// noDate builds a filter for items where date is not set
noDate() {
if (this._filterState == 'noDate') { this.reset(); return; }
if (this._filterState == 'noDate') {
this.reset();
return;
}
this._filter = null;
this._updateState('noDate');
}

View file

@ -4,11 +4,11 @@
var Meteor = Package.meteor.Meteor;
var global = Package.meteor.global;
var meteorEnv = Package.meteor.meteorEnv;
var FS = Package['cfs:base-package'].FS;
var FS = Package['wekan-cfs-base-package'].FS;
var check = Package.check.check;
var Match = Package.check.Match;
var EJSON = Package.ejson.EJSON;
var HTTP = Package['cfs:http-methods'].HTTP;
var HTTP = Package['wekan-cfs-http-methods'].HTTP;
/* Package-scope variables */
var rootUrlPathPrefix, baseUrl, getHeaders, getHeadersByCollection, _existingMountPoints, mountUrls;
@ -25,7 +25,7 @@ var rootUrlPathPrefix, baseUrl, getHeaders, getHeadersByCollection, _existingMou
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// //
// packages/cfs:access-point/access-point-common.js //
// packages/wekan-cfs-access-point/access-point-common.js //
// //
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//
@ -217,7 +217,7 @@ FS.File.prototype.url = function(options) {
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// //
// packages/cfs:access-point/access-point-handlers.js //
// packages/wekan-cfs-access-point/access-point-handlers.js //
// //
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//
@ -537,7 +537,7 @@ FS.HTTP.Handlers.PutUpdate = function httpPutUpdateHandler(ref) {
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// //
// packages/cfs:access-point/access-point-server.js //
// packages/wekan-cfs-access-point/access-point-server.js //
// //
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//
@ -909,6 +909,6 @@ Meteor.startup(function () {
/* Exports */
if (typeof Package === 'undefined') Package = {};
Package['cfs:access-point'] = {};
Package['wekan-cfs-access-point'] = {};
})();

View file

@ -650,7 +650,7 @@ Cards.helpers({
allSubtasks() {
return Cards.find({
parentId: this._id
parentId: this._id,
});
},

View file

@ -385,7 +385,8 @@ export class ExporterExcel {
//get kanban swimlanes info
const jswimlane = {};
for (const kswimlane in result.swimlanes) {
jswimlane[result.swimlanes[kswimlane]._id] = result.swimlanes[kswimlane].title;
jswimlane[result.swimlanes[kswimlane]._id] =
result.swimlanes[kswimlane].title;
}
//get kanban label info
const jlabel = {};
@ -394,8 +395,7 @@ export class ExporterExcel {
console.log(klabel);
if (isFirst == 0) {
jlabel[result.labels[klabel]._id] = `,${result.labels[klabel].name}`;
}
else{
} else {
isFirst = 0;
jlabel[result.labels[klabel]._id] = result.labels[klabel].name;
}

5
package-lock.json generated
View file

@ -720,11 +720,6 @@
"lodash.uniq": "^4.5.0"
}
},
"@root/request": {
"version": "1.6.1",
"resolved": "https://registry.npmjs.org/@root/request/-/request-1.6.1.tgz",
"integrity": "sha512-8wrWyeBLRp7T8J36GkT3RODJ6zYmL0/maWlAUD5LOXT28D3TDquUepyYDKYANNA3Gc8R5ZCgf+AXvSTYpJEWwQ=="
},
"@samverschueren/stream-to-observable": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/@samverschueren/stream-to-observable/-/stream-to-observable-0.3.0.tgz",

View file

@ -56,7 +56,6 @@
"dependencies": {
"@babel/core": "^7.9.6",
"@babel/runtime": "^7.9.6",
"@root/request": "^1.6.1",
"ajv": "^6.12.4",
"babel-runtime": "^6.26.0",
"bcrypt": "^5.0.0",
@ -67,7 +66,7 @@
"fibers": "^5.0.0",
"flatted": "^3.0.4",
"gridfs-stream": "https://github.com/wekan/gridfs-stream/tarball/master",
"jszip": "^3.4.0",
"jszip": "^3.6.0",
"ldapjs": "^2.1.1",
"markdown-it": "^12.0.2",
"markdown-it-emoji": "^2.0.0",

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)

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,20 @@
The MIT License (MIT)
Copyright (c) 2013-2015 [@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,11 @@
wekan-cfs-base-package
=========================
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.
This package provides the `FS` namespace and helper methods used by many
CollectionFS packages.

View file

@ -0,0 +1,213 @@
## cfs-base-package Public API ##
CollectionFS, Base package
_API documentation automatically generated by [docmeteor](https://github.com/raix/docmeteor)._
#############################################################################
HELPERS
#############################################################################
-
### <a name="FS.Utility.cloneFileRecord"></a>*fsUtility*.cloneFileRecord(rec, [options])&nbsp;&nbsp;<sub><i>Anywhere</i></sub> ###
*This method __cloneFileRecord__ is defined in `FS.Utility`*
__Arguments__
* __rec__ *{[FS.File](#FS.File)|[FS.Collection filerecord](#FS.Collection filerecord)}*
* __options__ *{Object}* (Optional)
* __full__ *{Boolean}* (Optional, Default = false)
Set `true` to prevent certain properties from being omitted from the clone.
__Returns__ *{Object}*
Cloned filerecord
Makes a shallow clone of `rec`, filtering out some properties that might be present if
it's an FS.File instance, but which we never want to be part of the stored
filerecord.
This is a blacklist clone rather than a whitelist because we want the user to be able
to specify whatever additional properties they wish.
In general, we expect the following whitelist properties used by the internal and
external APIs:
_id, name, size, type, chunkCount, chunkSize, chunkSum, copies, createdAt, updatedAt, uploadedAt
Those properties, and any additional properties added by the user, should be present
in the returned object, which is suitable for inserting into the backing collection or
extending an FS.File instance.
> ```FS.Utility.cloneFileRecord = function(rec, options) { ...``` [base-common.js:71](base-common.js#L71)
-
### <a name="FS.Utility.defaultCallback"></a>*fsUtility*.defaultCallback([err])&nbsp;&nbsp;<sub><i>Anywhere</i></sub> ###
*This method __defaultCallback__ is defined in `FS.Utility`*
__Arguments__
* __err__ *{[Error](#Error)}* (Optional)
__Returns__ *{undefined}*
Can be used as a default callback for client methods that need a callback.
Simply throws the provided error if there is one.
> ```FS.Utility.defaultCallback = function defaultCallback(err) { ...``` [base-common.js:96](base-common.js#L96)
-
### <a name="FS.Utility.defaultCallback"></a>*fsUtility*.defaultCallback([f], [err])&nbsp;&nbsp;<sub><i>Anywhere</i></sub> ###
*This method __defaultCallback__ is defined in `FS.Utility`*
__Arguments__
* __f__ *{Function}* (Optional)
A callback function, if you have one. Can be undefined or null.
* __err__ *{[Meteor.Error ](#Meteor.Error )|[ Error ](# Error )|[ String](# String)}* (Optional)
Error or error message (string)
__Returns__ *{Any}*
the callback result if any
Handle Error, creates an Error instance with the given text. If callback is
a function, passes the error to that function. Otherwise throws it. Useful
for dealing with errors in methods that optionally accept a callback.
> ```FS.Utility.handleError = function(f, err, result) { ...``` [base-common.js:120](base-common.js#L120)
-
### <a name="FS.Utility.noop"></a>*fsUtility*.noop()&nbsp;&nbsp;<sub><i>Anywhere</i></sub> ###
*This method __noop__ is defined in `FS.Utility`*
Use this to hand a no operation / empty function
> ```FS.Utility.noop = function() { ...``` [base-common.js:134](base-common.js#L134)
-
### <a name="FS.Utility.getFileExtension"></a>*fsUtility*.getFileExtension(name)&nbsp;&nbsp;<sub><i>Anywhere</i></sub> ###
*This method __getFileExtension__ is defined in `FS.Utility`*
__Arguments__
* __name__ *{String}*
A filename, filepath, or URL that may or may not have an extension.
__Returns__ *{String}*
The extension or an empty string if no extension found.
> ```FS.Utility.getFileExtension = function utilGetFileExtension(name) { ...``` [base-common.js:205](base-common.js#L205)
-
### <a name="FS.Utility.setFileExtension"></a>*fsUtility*.setFileExtension(name, ext)&nbsp;&nbsp;<sub><i>Anywhere</i></sub> ###
*This method __setFileExtension__ is defined in `FS.Utility`*
__Arguments__
* __name__ *{String}*
A filename that may or may not already have an extension.
* __ext__ *{String}*
An extension without leading period, which you want to be the new extension on `name`.
__Returns__ *{String}*
The filename with changed extension.
> ```FS.Utility.setFileExtension = function utilSetFileExtension(name, ext) { ...``` [base-common.js:222](base-common.js#L222)
-
### <a name="FS.Utility.binaryToBuffer"></a>*fsUtility*.binaryToBuffer(data)&nbsp;&nbsp;<sub><i>Server</i></sub> ###
*This method __binaryToBuffer__ is defined in `FS.Utility`*
__Arguments__
* __data__ *{Uint8Array}*
__Returns__ *{Buffer}*
Converts a Uint8Array instance to a Node Buffer instance
> ```FS.Utility.binaryToBuffer = function(data) { ...``` [base-server.js:9](base-server.js#L9)
-
### <a name="FS.Utility.bufferToBinary"></a>*fsUtility*.bufferToBinary(data)&nbsp;&nbsp;<sub><i>Server</i></sub> ###
*This method __bufferToBinary__ is defined in `FS.Utility`*
__Arguments__
* __data__ *{Buffer}*
__Returns__ *{Uint8Array}*
Converts a Node Buffer instance to a Uint8Array instance
> ```FS.Utility.bufferToBinary = function(data) { ...``` [base-server.js:26](base-server.js#L26)
-
### <a name="FS.Utility.eachFile"></a>*fsUtility*.eachFile(e, f)&nbsp;&nbsp;<sub><i>Client</i></sub> ###
*This method __eachFile__ is defined in `FS.Utility`*
__Arguments__
* __e__ *{[Event](#Event)}*
Browser event
* __f__ *{Function}*
Function to run for each file found in the event.
__Returns__ *{undefined}*
Utility for iteration over files in event
> ```FS.Utility.eachFile = function(e, f) { ...``` [base-client.js:37](base-client.js#L37)

View file

@ -0,0 +1,51 @@
//XXX not sure this is still working properly?
FS.Utility.connectionLogin = function(connection) {
// We check if the accounts package is installed, since we depend on
// `Meteor.userId()`
if (typeof Accounts !== 'undefined') {
// Monitor logout from main connection
Meteor.startup(function() {
Tracker.autorun(function() {
var userId = Meteor.userId();
if (userId) {
connection.onReconnect = function() {
var token = Accounts._storedLoginToken();
connection.apply('login', [{resume: token}], function(err, result) {
if (!err && result) {
connection.setUserId(result.id);
}
});
};
} else {
connection.onReconnect = null;
connection.setUserId(null);
}
});
});
}
};
/**
* @method FS.Utility.eachFile
* @public
* @param {Event} e - Browser event
* @param {Function} f - Function to run for each file found in the event.
* @returns {undefined}
*
* Utility for iteration over files in event
*/
FS.Utility.eachFile = function(e, f) {
var evt = (e.originalEvent || e);
var files = evt.target.files;
if (!files || files.length === 0) {
files = evt.dataTransfer ? evt.dataTransfer.files : [];
}
for (var i = 0; i < files.length; i++) {
f(files[i], i);
}
};

View file

@ -0,0 +1,317 @@
// Exported namespace
FS = {};
// namespace for adapters; XXX should this be added by cfs-storage-adapter pkg instead?
FS.Store = {
GridFS: function () {
throw new Error('To use FS.Store.GridFS, you must add the "wekan-cfs-gridfs" package.');
},
FileSystem: function () {
throw new Error('To use FS.Store.FileSystem, you must add the "wekan-cfs-filesystem" package.');
},
S3: function () {
throw new Error('To use FS.Store.S3, you must add the "wekan-cfs-s3" package.');
},
WABS: function () {
throw new Error('To use FS.Store.WABS, you must add the "wekan-cfs-wabs" package.');
},
Dropbox: function () {
throw new Error('To use FS.Store.Dropbox, you must add the "wekan-cfs-dropbox" package.');
}
};
// namespace for access points
FS.AccessPoint = {};
// namespace for utillities
FS.Utility = {};
// A general place for any package to store global config settings
FS.config = {};
// An internal collection reference
FS._collections = {};
// Test scope
_Utility = {};
// #############################################################################
//
// HELPERS
//
// #############################################################################
/** @method _Utility.defaultZero
* @private
* @param {Any} val Returns number or 0 if value is a falsy
*/
_Utility.defaultZero = function(val) {
return +(val || 0);
};
/**
* @method FS.Utility.cloneFileRecord
* @public
* @param {FS.File|FS.Collection filerecord} rec
* @param {Object} [options]
* @param {Boolean} [options.full=false] Set `true` to prevent certain properties from being omitted from the clone.
* @returns {Object} Cloned filerecord
*
* Makes a shallow clone of `rec`, filtering out some properties that might be present if
* it's an FS.File instance, but which we never want to be part of the stored
* filerecord.
*
* This is a blacklist clone rather than a whitelist because we want the user to be able
* to specify whatever additional properties they wish.
*
* In general, we expect the following whitelist properties used by the internal and
* external APIs:
*
* _id, name, size, type, chunkCount, chunkSize, chunkSum, copies, createdAt, updatedAt, uploadedAt
*
* Those properties, and any additional properties added by the user, should be present
* in the returned object, which is suitable for inserting into the backing collection or
* extending an FS.File instance.
*
*/
FS.Utility.cloneFileRecord = function(rec, options) {
options = options || {};
var result = {};
// We use this method for two purposes. If using it to clone one FS.File into another, then
// we want a full clone. But if using it to get a filerecord object for inserting into the
// internal collection, then there are certain properties we want to omit so that they aren't
// stored in the collection.
var omit = options.full ? [] : ['collectionName', 'collection', 'data', 'createdByTransform'];
for (var prop in rec) {
if (rec.hasOwnProperty(prop) && !_.contains(omit, prop)) {
result[prop] = rec[prop];
}
}
return result;
};
/**
* @method FS.Utility.defaultCallback
* @public
* @param {Error} [err]
* @returns {undefined}
*
* Can be used as a default callback for client methods that need a callback.
* Simply throws the provided error if there is one.
*/
FS.Utility.defaultCallback = function defaultCallback(err) {
if (err) {
// Show gentle error if Meteor error
if (err instanceof Meteor.Error) {
console.error(err.message);
} else {
// Normal error, just throw error
throw err;
}
}
};
/**
* @method FS.Utility.defaultCallback
* @public
* @param {Function} [f] A callback function, if you have one. Can be undefined or null.
* @param {Meteor.Error | Error | String} [err] Error or error message (string)
* @returns {Any} the callback result if any
*
* Handle Error, creates an Error instance with the given text. If callback is
* a function, passes the error to that function. Otherwise throws it. Useful
* for dealing with errors in methods that optionally accept a callback.
*/
FS.Utility.handleError = function(f, err, result) {
// Set callback
var callback = (typeof f === 'function')? f : FS.Utility.defaultCallback;
// Set the err
var error = (err === ''+err)? new Error(err) : err;
// callback
return callback(error, result);
}
/**
* @method FS.Utility.noop
* @public
* Use this to hand a no operation / empty function
*/
FS.Utility.noop = function() {};
/**
* @method validateAction
* @private
* @param {Object} validators - The validators object to use, with `deny` and `allow` properties.
* @param {FS.File} fileObj - Mounted or mountable file object to be passed to validators.
* @param {String} userId - The ID of the user who is attempting the action.
* @returns {undefined}
*
* Throws a "400-Bad Request" Meteor error if the file is not mounted or
* a "400-Access denied" Meteor error if the action is not allowed.
*/
FS.Utility.validateAction = function validateAction(validators, fileObj, userId) {
var denyValidators = validators.deny;
var allowValidators = validators.allow;
// If insecure package is used and there are no validators defined,
// allow the action.
if (typeof Package === 'object'
&& Package.insecure
&& denyValidators.length + allowValidators.length === 0) {
return;
}
// If already mounted, validators should receive a fileObj
// that is fully populated
if (fileObj.isMounted()) {
fileObj.getFileRecord();
}
// Any deny returns true means denied.
if (_.any(denyValidators, function(validator) {
return validator(userId, fileObj);
})) {
throw new Meteor.Error(403, "Access denied");
}
// Any allow returns true means proceed. Throw error if they all fail.
if (_.all(allowValidators, function(validator) {
return !validator(userId, fileObj);
})) {
throw new Meteor.Error(403, "Access denied");
}
};
/**
* @method FS.Utility.getFileName
* @private
* @param {String} name - A filename, filepath, or URL
* @returns {String} The filename without the URL, filepath, or query string
*/
FS.Utility.getFileName = function utilGetFileName(name) {
// in case it's a URL, strip off potential query string
// should have no effect on filepath
name = name.split('?')[0];
// strip off beginning path or url
var lastSlash = name.lastIndexOf('/');
if (lastSlash !== -1) {
name = name.slice(lastSlash + 1);
}
return name;
};
/**
* @method FS.Utility.getFileExtension
* @public
* @param {String} name - A filename, filepath, or URL that may or may not have an extension.
* @returns {String} The extension or an empty string if no extension found.
*/
FS.Utility.getFileExtension = function utilGetFileExtension(name) {
name = FS.Utility.getFileName(name);
// Seekout the last '.' if found
var found = name.lastIndexOf('.');
// Return the extension if found else ''
// If found is -1, we return '' because there is no extension
// If found is 0, we return '' because it's a hidden file
return (found > 0 ? name.slice(found + 1).toLowerCase() : '');
};
/**
* @method FS.Utility.setFileExtension
* @public
* @param {String} name - A filename that may or may not already have an extension.
* @param {String} ext - An extension without leading period, which you want to be the new extension on `name`.
* @returns {String} The filename with changed extension.
*/
FS.Utility.setFileExtension = function utilSetFileExtension(name, ext) {
if (!name || !name.length) {
return name;
}
var currentExt = FS.Utility.getFileExtension(name);
if (currentExt.length) {
name = name.slice(0, currentExt.length * -1) + ext;
} else {
name = name + '.' + ext;
}
return name;
};
/*
* Borrowed these from http package
*/
FS.Utility.encodeParams = function encodeParams(params) {
var buf = [];
_.each(params, function(value, key) {
if (buf.length)
buf.push('&');
buf.push(FS.Utility.encodeString(key), '=', FS.Utility.encodeString(value));
});
return buf.join('').replace(/%20/g, '+');
};
FS.Utility.encodeString = function encodeString(str) {
return encodeURIComponent(str).replace(/[!'()]/g, escape).replace(/\*/g, "%2A");
};
/*
* btoa and atob shims for client and server
*/
FS.Utility._btoa = function _fsUtility_btoa(str) {
var buffer;
if (str instanceof Buffer) {
buffer = str;
} else {
buffer = new Buffer(str.toString(), 'binary');
}
return buffer.toString('base64');
};
FS.Utility.btoa = function fsUtility_btoa(str) {
if (typeof btoa === 'function') {
// Client
return btoa(str);
} else if (typeof Buffer !== 'undefined') {
// Server
return FS.Utility._btoa(str);
} else {
throw new Error('FS.Utility.btoa: Cannot base64 encode on your system');
}
};
FS.Utility._atob = function _fsUtility_atob(str) {
return new Buffer(str, 'base64').toString('binary');
};
FS.Utility.atob = function fsUtility_atob(str) {
if (typeof atob === 'function') {
// Client
return atob(str);
} else if (typeof Buffer !== 'undefined') {
// Server
return FS.Utility._atob(str);
} else {
throw new Error('FS.Utility.atob: Cannot base64 encode on your system');
}
};
// Api wrap for 3party libs like underscore
FS.Utility.extend = _.extend;
FS.Utility.each = _.each;
FS.Utility.isEmpty = _.isEmpty;
FS.Utility.indexOf = _.indexOf;
FS.Utility.isArray = _.isArray;
FS.Utility.map = _.map;
FS.Utility.once = _.once;
FS.Utility.include = _.include;
FS.Utility.size = _.size;

View file

@ -0,0 +1,95 @@
/**
* @method FS.Utility.binaryToBuffer
* @public
* @param {Uint8Array} data
* @returns {Buffer}
*
* Converts a Uint8Array instance to a Node Buffer instance
*/
FS.Utility.binaryToBuffer = function(data) {
var len = data.length;
var buffer = new Buffer(len);
for (var i = 0; i < len; i++) {
buffer[i] = data[i];
}
return buffer;
};
/**
* @method FS.Utility.bufferToBinary
* @public
* @param {Buffer} data
* @returns {Uint8Array}
*
* Converts a Node Buffer instance to a Uint8Array instance
*/
FS.Utility.bufferToBinary = function(data) {
var len = data.length;
var binary = EJSON.newBinary(len);
for (var i = 0; i < len; i++) {
binary[i] = data[i];
}
return binary;
};
/**
* @method FS.Utility.safeCallback
* @public
* @param {Function} callback
* @returns {Function}
*
* Makes a callback safe for Meteor code
*/
FS.Utility.safeCallback = function (callback) {
return Meteor.bindEnvironment(callback, function(err) { throw err; });
};
/**
* @method FS.Utility.safeStream
* @public
* @param {Stream} nodestream
* @returns {Stream}
*
* Adds `safeOn` and `safeOnce` methods to a NodeJS Stream
* object. These are the same as `on` and `once`, except
* that the callback is wrapped for use in Meteor.
*/
FS.Utility.safeStream = function(nodestream) {
if (!nodestream || typeof nodestream.on !== 'function')
throw new Error('FS.Utility.safeStream requires a NodeJS Stream');
// Create Meteor safe events
nodestream.safeOn = function(name, callback) {
return nodestream.on(name, FS.Utility.safeCallback(callback));
};
// Create Meteor safe events
nodestream.safeOnce = function(name, callback) {
return nodestream.once(name, FS.Utility.safeCallback(callback));
};
// Return the modified stream - modified anyway
return nodestream;
};
/**
* @method FS.Utility.eachFileFromPath
* @public
* @param {String} p - Server path
* @param {Function} f - Function to run for each file found in the path.
* @returns {undefined}
*
* Utility for iteration over files from path on server
*/
FS.Utility.eachFileFromPath = function(p, f) {
var fs = Npm.require('fs');
var path = Npm.require('path');
var files = fs.readdirSync(p);
files.map(function (file) {
return path.join(p, file);
}).filter(function (filePath) {
return fs.statSync(filePath).isFile() && path.basename(filePath)[0] !== '.';
}).forEach(function (filePath) {
f(filePath);
});
};

View file

@ -0,0 +1,293 @@
## Public and Private API ##
_API documentation automatically generated by [docmeteor](https://github.com/raix/docmeteor)._
***
__File: ["base-common.js"](base-common.js) Where: {server|client}__
***
#############################################################################
HELPERS
#############################################################################
-
### <a name="_Utility.defaultZero"></a>*_utility*.defaultZero(val)&nbsp;&nbsp;<sub><i>Anywhere</i></sub> ###
*This method is private*
*This method __defaultZero__ is defined in `_Utility`*
__Arguments__
* __val__ *{Any}*
Returns number or 0 if value is a falsy
> ```_Utility.defaultZero = function(val) { ...``` [base-common.js:42](base-common.js#L42)
-
### <a name="FS.Utility.cloneFileRecord"></a>*fsUtility*.cloneFileRecord(rec, [options])&nbsp;&nbsp;<sub><i>Anywhere</i></sub> ###
*This method __cloneFileRecord__ is defined in `FS.Utility`*
__Arguments__
* __rec__ *{[FS.File](#FS.File)|[FS.Collection filerecord](#FS.Collection filerecord)}*
* __options__ *{Object}* (Optional)
* __full__ *{Boolean}* (Optional, Default = false)
Set `true` to prevent certain properties from being omitted from the clone.
__Returns__ *{Object}*
Cloned filerecord
Makes a shallow clone of `rec`, filtering out some properties that might be present if
it's an FS.File instance, but which we never want to be part of the stored
filerecord.
This is a blacklist clone rather than a whitelist because we want the user to be able
to specify whatever additional properties they wish.
In general, we expect the following whitelist properties used by the internal and
external APIs:
_id, name, size, type, chunkCount, chunkSize, chunkSum, copies, createdAt, updatedAt, uploadedAt
Those properties, and any additional properties added by the user, should be present
in the returned object, which is suitable for inserting into the backing collection or
extending an FS.File instance.
> ```FS.Utility.cloneFileRecord = function(rec, options) { ...``` [base-common.js:71](base-common.js#L71)
-
### <a name="FS.Utility.defaultCallback"></a>*fsUtility*.defaultCallback([err])&nbsp;&nbsp;<sub><i>Anywhere</i></sub> ###
*This method __defaultCallback__ is defined in `FS.Utility`*
__Arguments__
* __err__ *{[Error](#Error)}* (Optional)
__Returns__ *{undefined}*
Can be used as a default callback for client methods that need a callback.
Simply throws the provided error if there is one.
> ```FS.Utility.defaultCallback = function defaultCallback(err) { ...``` [base-common.js:96](base-common.js#L96)
-
### <a name="FS.Utility.defaultCallback"></a>*fsUtility*.defaultCallback([f], [err])&nbsp;&nbsp;<sub><i>Anywhere</i></sub> ###
*This method __defaultCallback__ is defined in `FS.Utility`*
__Arguments__
* __f__ *{Function}* (Optional)
A callback function, if you have one. Can be undefined or null.
* __err__ *{[Meteor.Error ](#Meteor.Error )|[ Error ](# Error )|[ String](# String)}* (Optional)
Error or error message (string)
__Returns__ *{Any}*
the callback result if any
Handle Error, creates an Error instance with the given text. If callback is
a function, passes the error to that function. Otherwise throws it. Useful
for dealing with errors in methods that optionally accept a callback.
> ```FS.Utility.handleError = function(f, err, result) { ...``` [base-common.js:120](base-common.js#L120)
-
### <a name="FS.Utility.noop"></a>*fsUtility*.noop()&nbsp;&nbsp;<sub><i>Anywhere</i></sub> ###
*This method __noop__ is defined in `FS.Utility`*
Use this to hand a no operation / empty function
> ```FS.Utility.noop = function() { ...``` [base-common.js:134](base-common.js#L134)
-
### <a name="validateAction"></a>validateAction(validators, fileObj, userId)&nbsp;&nbsp;<sub><i>Anywhere</i></sub> ###
*This method is private*
__Arguments__
* __validators__ *{Object}*
The validators object to use, with `deny` and `allow` properties.
* __fileObj__ *{[FS.File](#FS.File)}*
Mounted or mountable file object to be passed to validators.
* __userId__ *{String}*
The ID of the user who is attempting the action.
__Returns__ *{undefined}*
Throws a "400-Bad Request" Meteor error if the file is not mounted or
a "400-Access denied" Meteor error if the action is not allowed.
> ```FS.Utility.validateAction = function validateAction(validators, fileObj, userId) { ...``` [base-common.js:147](base-common.js#L147)
-
### <a name="FS.Utility.getFileName"></a>*fsUtility*.getFileName(name)&nbsp;&nbsp;<sub><i>Anywhere</i></sub> ###
*This method is private*
*This method __getFileName__ is defined in `FS.Utility`*
__Arguments__
* __name__ *{String}*
A filename, filepath, or URL
__Returns__ *{String}*
The filename without the URL, filepath, or query string
> ```FS.Utility.getFileName = function utilGetFileName(name) { ...``` [base-common.js:187](base-common.js#L187)
-
### <a name="FS.Utility.getFileExtension"></a>*fsUtility*.getFileExtension(name)&nbsp;&nbsp;<sub><i>Anywhere</i></sub> ###
*This method __getFileExtension__ is defined in `FS.Utility`*
__Arguments__
* __name__ *{String}*
A filename, filepath, or URL that may or may not have an extension.
__Returns__ *{String}*
The extension or an empty string if no extension found.
> ```FS.Utility.getFileExtension = function utilGetFileExtension(name) { ...``` [base-common.js:205](base-common.js#L205)
-
### <a name="FS.Utility.setFileExtension"></a>*fsUtility*.setFileExtension(name, ext)&nbsp;&nbsp;<sub><i>Anywhere</i></sub> ###
*This method __setFileExtension__ is defined in `FS.Utility`*
__Arguments__
* __name__ *{String}*
A filename that may or may not already have an extension.
* __ext__ *{String}*
An extension without leading period, which you want to be the new extension on `name`.
__Returns__ *{String}*
The filename with changed extension.
> ```FS.Utility.setFileExtension = function utilSetFileExtension(name, ext) { ...``` [base-common.js:222](base-common.js#L222)
***
__File: ["base-server.js"](base-server.js) Where: {server}__
***
### <a name="FS.Utility.binaryToBuffer"></a>*fsUtility*.binaryToBuffer(data)&nbsp;&nbsp;<sub><i>Server</i></sub> ###
*This method __binaryToBuffer__ is defined in `FS.Utility`*
__Arguments__
* __data__ *{Uint8Array}*
__Returns__ *{Buffer}*
Converts a Uint8Array instance to a Node Buffer instance
> ```FS.Utility.binaryToBuffer = function(data) { ...``` [base-server.js:9](base-server.js#L9)
-
### <a name="FS.Utility.bufferToBinary"></a>*fsUtility*.bufferToBinary(data)&nbsp;&nbsp;<sub><i>Server</i></sub> ###
*This method __bufferToBinary__ is defined in `FS.Utility`*
__Arguments__
* __data__ *{Buffer}*
__Returns__ *{Uint8Array}*
Converts a Node Buffer instance to a Uint8Array instance
> ```FS.Utility.bufferToBinary = function(data) { ...``` [base-server.js:26](base-server.js#L26)
***
__File: ["base-client.js"](base-client.js) Where: {client}__
***
### <a name="FS.Utility.eachFile"></a>*fsUtility*.eachFile(e, f)&nbsp;&nbsp;<sub><i>Client</i></sub> ###
*This method __eachFile__ is defined in `FS.Utility`*
__Arguments__
* __e__ *{[Event](#Event)}*
Browser event
* __f__ *{Function}*
Function to run for each file found in the event.
__Returns__ *{undefined}*
Utility for iteration over files in event
> ```FS.Utility.eachFile = function(e, f) { ...``` [base-client.js:37](base-client.js#L37)

View file

@ -0,0 +1,37 @@
Package.describe({
version: '0.0.30',
name: 'wekan-cfs-base-package',
summary: 'CollectionFS, Base package',
git: 'https://github.com/zcfs/Meteor-cfs-base-package.git'
});
Package.onUse(function(api) {
api.versionsFrom('1.0');
api.use(['deps', 'underscore', 'ejson']);
if (api.export) {
api.export('FS');
api.export('_Utility', { testOnly: true });
}
api.addFiles([
'base-common.js',
'base-server.js'
], 'server');
api.addFiles([
'polyfill.base64.js',
'base-common.js',
'base-client.js'
], 'client');
});
// Package.on_test(function (api) {
// api.use(['wekan-cfs-base-package', 'cfs-file']);
// api.use('test-helpers', 'server');
// api.use(['tinytest', 'underscore', 'ejson', 'ordered-dict',
// 'random', 'deps']);
// api.add_files('tests/common-tests.js', ['client', 'server']);
// });

View file

@ -0,0 +1,179 @@
/*
* Copyright (c) 2010 Nick Galbreath
* http://code.google.com/p/stringencoders/source/browse/#svn/trunk/javascript
*
* 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.
*/
/* base64 encode/decode compatible with window.btoa/atob
*
* window.atob/btoa is a Firefox extension to convert binary data (the "b")
* to base64 (ascii, the "a").
*
* It is also found in Safari and Chrome. It is not available in IE.
*
* if (!window.btoa) window.btoa = base64.encode
* if (!window.atob) window.atob = base64.decode
*
* The original spec's for atob/btoa are a bit lacking
* https://developer.mozilla.org/en/DOM/window.atob
* https://developer.mozilla.org/en/DOM/window.btoa
*
* window.btoa and base64.encode takes a string where charCodeAt is [0,255]
* If any character is not [0,255], then an DOMException(5) is thrown.
*
* window.atob and base64.decode take a base64-encoded string
* If the input length is not a multiple of 4, or contains invalid characters
* then an DOMException(5) is thrown.
*/
var base64 = {};
base64.PADCHAR = '=';
base64.ALPHA = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
base64.makeDOMException = function() {
// sadly in FF,Safari,Chrome you can't make a DOMException
var e, tmp;
try {
return new DOMException(DOMException.INVALID_CHARACTER_ERR);
} catch (tmp) {
// not available, just passback a duck-typed equiv
// https://developer.mozilla.org/en/Core_JavaScript_1.5_Reference/Global_Objects/Error
// https://developer.mozilla.org/en/Core_JavaScript_1.5_Reference/Global_Objects/Error/prototype
var ex = new Error("DOM Exception 5");
// ex.number and ex.description is IE-specific.
ex.code = ex.number = 5;
ex.name = ex.description = "INVALID_CHARACTER_ERR";
// Safari/Chrome output format
ex.toString = function() { return 'Error: ' + ex.name + ': ' + ex.message; };
return ex;
}
}
base64.getbyte64 = function(s,i) {
// This is oddly fast, except on Chrome/V8.
// Minimal or no improvement in performance by using a
// object with properties mapping chars to value (eg. 'A': 0)
var idx = base64.ALPHA.indexOf(s.charAt(i));
if (idx === -1) {
throw base64.makeDOMException();
}
return idx;
}
base64.decode = function(s) {
// convert to string
s = '' + s;
var getbyte64 = base64.getbyte64;
var pads, i, b10;
var imax = s.length
if (imax === 0) {
return s;
}
if (imax % 4 !== 0) {
throw base64.makeDOMException();
}
pads = 0
if (s.charAt(imax - 1) === base64.PADCHAR) {
pads = 1;
if (s.charAt(imax - 2) === base64.PADCHAR) {
pads = 2;
}
// either way, we want to ignore this last block
imax -= 4;
}
var x = [];
for (i = 0; i < imax; i += 4) {
b10 = (getbyte64(s,i) << 18) | (getbyte64(s,i+1) << 12) |
(getbyte64(s,i+2) << 6) | getbyte64(s,i+3);
x.push(String.fromCharCode(b10 >> 16, (b10 >> 8) & 0xff, b10 & 0xff));
}
switch (pads) {
case 1:
b10 = (getbyte64(s,i) << 18) | (getbyte64(s,i+1) << 12) | (getbyte64(s,i+2) << 6);
x.push(String.fromCharCode(b10 >> 16, (b10 >> 8) & 0xff));
break;
case 2:
b10 = (getbyte64(s,i) << 18) | (getbyte64(s,i+1) << 12);
x.push(String.fromCharCode(b10 >> 16));
break;
}
return x.join('');
}
base64.getbyte = function(s,i) {
var x = s.charCodeAt(i);
if (x > 255) {
throw base64.makeDOMException();
}
return x;
}
base64.encode = function(s) {
if (arguments.length !== 1) {
throw new SyntaxError("Not enough arguments");
}
var padchar = base64.PADCHAR;
var alpha = base64.ALPHA;
var getbyte = base64.getbyte;
var i, b10;
var x = [];
// convert to string
s = '' + s;
var imax = s.length - s.length % 3;
if (s.length === 0) {
return s;
}
for (i = 0; i < imax; i += 3) {
b10 = (getbyte(s,i) << 16) | (getbyte(s,i+1) << 8) | getbyte(s,i+2);
x.push(alpha.charAt(b10 >> 18));
x.push(alpha.charAt((b10 >> 12) & 0x3F));
x.push(alpha.charAt((b10 >> 6) & 0x3f));
x.push(alpha.charAt(b10 & 0x3f));
}
switch (s.length - imax) {
case 1:
b10 = getbyte(s,i) << 16;
x.push(alpha.charAt(b10 >> 18) + alpha.charAt((b10 >> 12) & 0x3F) +
padchar + padchar);
break;
case 2:
b10 = (getbyte(s,i) << 16) | (getbyte(s,i+1) << 8);
x.push(alpha.charAt(b10 >> 18) + alpha.charAt((b10 >> 12) & 0x3F) +
alpha.charAt((b10 >> 6) & 0x3f) + padchar);
break;
}
return x.join('');
}
if (!window.btoa) window.btoa = base64.encode
if (!window.atob) window.atob = base64.decode

View file

@ -0,0 +1,161 @@
function equals(a, b) {
return EJSON.stringify(a) === EJSON.stringify(b);
}
Tinytest.add('cfs-base-package - test environment', function(test) {
test.isTrue(typeof FS !== 'undefined',
'FS scope not declared');
test.isTrue(typeof FS.Store !== 'undefined',
'FS scope "FS.Store" not declared');
test.isTrue(typeof FS.AccessPoint !== 'undefined',
'FS scope "FS.AccessPoint" not declared');
test.isTrue(typeof FS.Utility !== 'undefined',
'FS scope "FS.Utility" not declared');
test.isTrue(typeof FS._collections !== 'undefined',
'FS scope "FS._collections" not declared');
test.isTrue(typeof _Utility !== 'undefined',
'_Utility test scope not declared');
});
Tinytest.add('cfs-base-package - _Utility.defaultZero', function(test) {
test.equal(_Utility.defaultZero(), 0, 'Failes to return 0 when (undefined)');
test.equal(_Utility.defaultZero(undefined), 0, 'Failes to return 0 when undefined');
test.equal(_Utility.defaultZero(null), 0, 'Failes to return 0 when null');
test.equal(_Utility.defaultZero(false), 0, 'Failes to return 0 when false');
test.equal(_Utility.defaultZero(0), 0, 'Failes to return 0 when 0');
test.equal(_Utility.defaultZero(-1), -1, 'Failes to return -1');
test.equal(_Utility.defaultZero(1), 1, 'Failes to return 1');
test.equal(_Utility.defaultZero(-0.1), -0.1, 'Failes to return -0.1');
test.equal(_Utility.defaultZero(0.1), 0.1, 'Failes to return 0.1');
test.equal(_Utility.defaultZero(''), 0, 'Failes to return ""');
test.equal(_Utility.defaultZero({}), NaN, 'Failes to return NaN when object');
test.equal(_Utility.defaultZero("dfdsfs"), NaN, 'Failes to return NaN when string');
test.equal(_Utility.defaultZero("1"), 1, 'Failes to return 1 when string "1"');
});
Tinytest.add('cfs-base-package - FS.Utility.cloneFileRecord', function(test) {
// Given an object with any props, should filter out 'collectionName',
// 'collection', 'data', and 'createdByTransform'
var result = FS.Utility.cloneFileRecord({a: 1, b: {c: 1}, d: [1, 2], collectionName: 'test', collection: {}, data: {}, createdByTransform: false});
test.equal(result, {a: 1, b: {c: 1}, d: [1, 2]});
// Given an FS.File instance, should filter out 'collectionName',
// 'collection', 'data', and 'createdByTransform' and return a plain Object
var fileObj = new FS.File({a: 1, b: {c: 1}, d: [1, 2], name: 'name.png', type: 'image/png', size: 100, collectionName: 'test', collection: {}, data: {}, createdByTransform: false});
test.isTrue(fileObj instanceof FS.File);
var result = FS.Utility.cloneFileRecord(fileObj);
test.isFalse(result instanceof FS.File);
test.isTrue(equals(result, {a: 1, b: {c: 1}, d: [1, 2], name: 'name.png', type: 'image/png', size: 100}));
});
Tinytest.add('cfs-base-package - FS.Utility.defaultCallback', function(test) {
// should throw an error passed in, but not a Meteor.Error
test.throws(function () {
var cb = FS.Utility.defaultCallback;
cb(new Error('test'));
});
var cb2 = FS.Utility.defaultCallback;
test.isUndefined(cb2(new Meteor.Error('test')));
});
Tinytest.add('cfs-base-package - FS.Utility.handleError', function(test) {
test.isTrue(true);
// TODO
});
Tinytest.add('cfs-base-package - FS.Utility.binaryToBuffer', function(test) {
test.isTrue(true);
// TODO
});
Tinytest.add('cfs-base-package - FS.Utility.bufferToBinary', function(test) {
test.isTrue(true);
// TODO
});
Tinytest.add('cfs-base-package - FS.Utility.connectionLogin', function(test) {
test.isTrue(true);
// TODO
});
Tinytest.add('cfs-base-package - FS.Utility.getFileName', function(test) {
function t(input, expected) {
var ext = FS.Utility.getFileName(input);
test.equal(ext, expected, 'Got incorrect filename');
}
t('bar.png', 'bar.png');
t('foo/bar.png', 'bar.png');
t('/foo/foo/bar.png', 'bar.png');
t('http://foobar.com/file.png', 'file.png');
t('http://foobar.com/file', 'file');
t('http://foobar.com/file.png?a=b', 'file.png');
t('http://foobar.com/.file?a=b', '.file');
t('file', 'file');
t('.file', '.file');
t('foo/.file', '.file');
t('/foo/foo/.file', '.file');
});
Tinytest.add('cfs-base-package - FS.Utility.getFileExtension', function(test) {
function t(input, expected) {
var ext = FS.Utility.getFileExtension(input);
test.equal(ext, expected, 'Got incorrect extension');
}
t('bar.png', 'png');
t('foo/bar.png', 'png');
t('/foo/foo/bar.png', 'png');
t('http://foobar.com/file.png', 'png');
t('http://foobar.com/file', '');
t('http://foobar.com/file.png?a=b', 'png');
t('http://foobar.com/file?a=b', '');
t('file', '');
t('.file', '');
t('foo/.file', '');
t('/foo/foo/.file', '');
});
Tinytest.add('cfs-base-package - FS.Utility.setFileExtension', function(test) {
function t(name, ext, expected) {
var newName = FS.Utility.setFileExtension(name, ext);
test.equal(newName, expected, 'Extension was not set correctly');
}
t('bar.png', 'jpeg', 'bar.jpeg');
t('bar', 'jpeg', 'bar.jpeg');
t('.bar', 'jpeg', '.bar.jpeg');
t('', 'jpeg', '');
t(null, 'jpeg', null);
});
//Test API:
//Tinytest.add('', function(test) {});
//Tinytest.addAsync('', function(test, onComplete) {});
//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,5 @@
language: node_js
node_js:
- "0.10"
before_install:
- "curl -L http://git.io/s0Zu-w | /bin/sh"

View file

@ -0,0 +1,20 @@
The MIT License (MIT)
Copyright (c) 2013-2014 [@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,8 @@
wekan-cfs-collection-filters
=========================
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.

View file

@ -0,0 +1,44 @@
## cfs-collection-filters Public API ##
CollectionFS, adds FS.Collection filters
_API documentation automatically generated by [docmeteor](https://github.com/raix/docmeteor)._
-
### <a name="FS.Collection.prototype.filters"></a>*fsCollection*.filters(filters)&nbsp;&nbsp;<sub><i>Anywhere</i></sub> ###
*This method __filters__ is defined in `prototype` of `FS.Collection`*
__Arguments__
* __filters__ *{Object}*
File filters for this collection.
__Returns__ *{undefined}*
> ```FS.Collection.prototype.filters = function fsColFilters(filters) { ...``` [filters.js:7](filters.js#L7)
-
### <a name="FS.Collection.prototype.allowsFile"></a>*fsCollection*.allowsFile()&nbsp;&nbsp;<sub><i>Anywhere</i></sub> ###
*This method __allowsFile__ is defined in `prototype` of `FS.Collection`*
__Returns__ *{boolean}*
True if the collection allows this file.
Checks based on any filters defined on the collection. If the
file is not valid according to the filters, this method returns false
and also calls the filter `onInvalid` method defined for the
collection, passing it an English error string that explains why it
failed.
> ```FS.Collection.prototype.allowsFile = function fsColAllowsFile(fileObj) { ...``` [filters.js:108](filters.js#L108)

View file

@ -0,0 +1,191 @@
/**
* @method FS.Collection.prototype.filters
* @public
* @param {Object} filters - File filters for this collection.
* @returns {undefined}
*/
FS.Collection.prototype.filters = function fsColFilters(filters) {
var self = this;
// Check filter option values and normalize them for quicker checking later
if (filters) {
// check/adjust allow/deny
FS.Utility.each(['allow', 'deny'], function (type) {
if (!filters[type]) {
filters[type] = {};
} else if (typeof filters[type] !== "object") {
throw new Error(type + ' filter must be an object');
}
});
// check/adjust maxSize
if (typeof filters.maxSize === "undefined") {
filters.maxSize = null;
} else if (filters.maxSize && typeof filters.maxSize !== "number") {
throw new Error('maxSize filter must be an number');
}
// check/adjust extensions
FS.Utility.each(['allow', 'deny'], function (type) {
if (!filters[type].extensions) {
filters[type].extensions = [];
} else if (!FS.Utility.isArray(filters[type].extensions)) {
throw new Error(type + '.extensions filter must be an array of extensions');
} else {
//convert all to lowercase
for (var i = 0, ln = filters[type].extensions.length; i < ln; i++) {
filters[type].extensions[i] = filters[type].extensions[i].toLowerCase();
}
}
});
// check/adjust content types
FS.Utility.each(['allow', 'deny'], function (type) {
if (!filters[type].contentTypes) {
filters[type].contentTypes = [];
} else if (!FS.Utility.isArray(filters[type].contentTypes)) {
throw new Error(type + '.contentTypes filter must be an array of content types');
}
});
self.options.filter = filters;
}
// Define deny functions to enforce file filters on the server
// for inserts and updates that initiate from untrusted code.
self.files.deny({
insert: function(userId, fsFile) {
return !self.allowsFile(fsFile);
},
update: function(userId, fsFile, fields, modifier) {
// TODO will need some kind of additional security here:
// Don't allow them to change the type, size, name, and
// anything else that would be security or data integrity issue.
// Such security should probably be added by cfs-collection package, not here.
return !self.allowsFile(fsFile);
},
fetch: []
});
// If insecure package is in use, we need to add allow rules that return
// true. Otherwise, it would seemingly turn off insecure mode.
if (Package && Package.insecure) {
self.allow({
insert: function() {
return true;
},
update: function() {
return true;
},
remove: function() {
return true;
},
download: function() {
return true;
},
fetch: [],
transform: null
});
}
// If insecure package is NOT in use, then adding the deny function
// does not have any effect on the main app's security paradigm. The
// user will still be required to add at least one allow function of her
// own for each operation for this collection. And the user may still add
// additional deny functions, but does not have to.
};
/**
* @method FS.Collection.prototype.allowsFile Does the collection allow the specified file?
* @public
* @returns {boolean} True if the collection allows this file.
*
* Checks based on any filters defined on the collection. If the
* file is not valid according to the filters, this method returns false
* and also calls the filter `onInvalid` method defined for the
* collection, passing it an English error string that explains why it
* failed.
*/
FS.Collection.prototype.allowsFile = function fsColAllowsFile(fileObj) {
var self = this;
// Get filters
var filter = self.options.filter;
if (!filter) {
return true;
}
var saveAllFileExtensions = (filter.allow.extensions.length === 0);
var saveAllContentTypes = (filter.allow.contentTypes.length === 0);
// Get info about the file
var filename = fileObj.name();
var contentType = fileObj.type();
if (!saveAllContentTypes && !contentType) {
filter.onInvalid && filter.onInvalid(filename + " has an unknown content type");
return false;
}
var fileSize = fileObj.size();
if (!fileSize || isNaN(fileSize)) {
filter.onInvalid && filter.onInvalid(filename + " has an unknown file size");
return false;
}
// Do extension checks only if we have a filename
if (filename) {
var ext = fileObj.getExtension();
if (!((saveAllFileExtensions ||
FS.Utility.indexOf(filter.allow.extensions, ext) !== -1) &&
FS.Utility.indexOf(filter.deny.extensions, ext) === -1)) {
filter.onInvalid && filter.onInvalid(filename + ' has the extension "' + ext + '", which is not allowed');
return false;
}
}
// Do content type checks
if (!((saveAllContentTypes ||
contentTypeInList(filter.allow.contentTypes, contentType)) &&
!contentTypeInList(filter.deny.contentTypes, contentType))) {
filter.onInvalid && filter.onInvalid(filename + ' is of the type "' + contentType + '", which is not allowed');
return false;
}
// Do max size check
if (typeof filter.maxSize === "number" && fileSize > filter.maxSize) {
filter.onInvalid && filter.onInvalid(filename + " is too big");
return false;
}
return true;
};
/**
* @method contentTypeInList Is the content type string in the list?
* @private
* @param {String[]} list - Array of content types
* @param {String} contentType - The content type
* @returns {Boolean}
*
* Returns true if the content type is in the list, or if it matches
* one of the special types in the list, e.g., "image/*".
*/
function contentTypeInList(list, contentType) {
var listType, found = false;
for (var i = 0, ln = list.length; i < ln; i++) {
listType = list[i];
if (listType === contentType) {
found = true;
break;
}
if (listType === "image/*" && contentType.indexOf("image/") === 0) {
found = true;
break;
}
if (listType === "audio/*" && contentType.indexOf("audio/") === 0) {
found = true;
break;
}
if (listType === "video/*" && contentType.indexOf("video/") === 0) {
found = true;
break;
}
}
return found;
}

View file

@ -0,0 +1,72 @@
## Public and Private API ##
_API documentation automatically generated by [docmeteor](https://github.com/raix/docmeteor)._
***
__File: ["filters.js"](filters.js) Where: {client|server}__
***
### <a name="FS.Collection.prototype.filters"></a>*fsCollection*.filters(filters)&nbsp;&nbsp;<sub><i>Anywhere</i></sub> ###
*This method __filters__ is defined in `prototype` of `FS.Collection`*
__Arguments__
* __filters__ *{Object}*
File filters for this collection.
__Returns__ *{undefined}*
> ```FS.Collection.prototype.filters = function fsColFilters(filters) { ...``` [filters.js:7](filters.js#L7)
-
### <a name="FS.Collection.prototype.allowsFile"></a>*fsCollection*.allowsFile()&nbsp;&nbsp;<sub><i>Anywhere</i></sub> ###
*This method __allowsFile__ is defined in `prototype` of `FS.Collection`*
__Returns__ *{boolean}*
True if the collection allows this file.
Checks based on any filters defined on the collection. If the
file is not valid according to the filters, this method returns false
and also calls the filter `onInvalid` method defined for the
collection, passing it an English error string that explains why it
failed.
> ```FS.Collection.prototype.allowsFile = function fsColAllowsFile(fileObj) { ...``` [filters.js:108](filters.js#L108)
-
### <a name="contentTypeInList"></a>contentTypeInList(list, contentType)&nbsp;&nbsp;<sub><i>undefined</i></sub> ###
*This method is private*
__Arguments__
* __list__ *{[String[]](#String[])}*
Array of content types
* __contentType__ *{String}*
The content type
__Returns__ *{Boolean}*
Returns true if the content type is in the list, or if it matches
one of the special types in the list, e.g., "image/*".
> ```function contentTypeInList(list, contentType) { ...``` [filters.js:169](filters.js#L169)

View file

@ -0,0 +1,29 @@
Package.describe({
git: 'https://github.com/zcfs/Meteor-cfs-collection-filters.git',
name: 'wekan-cfs-collection-filters',
version: '0.2.4',
summary: 'CollectionFS, adds FS.Collection filters'
});
Package.onUse(function(api) {
api.versionsFrom('1.0');
api.use(['wekan-cfs-base-package@0.0.30', 'wekan-cfs-collection@0.5.4']);
api.addFiles([
'filters.js'
], 'client');
api.addFiles([
'filters.js'
], 'server');
});
// Package.on_test(function (api) {
// api.use('collectionfs');
// api.use('test-helpers', 'server');
// api.use(['tinytest']);
// api.addFiles('tests/server-tests.js', 'server');
// api.addFiles('tests/client-tests.js', 'client');
// });

View file

@ -0,0 +1,27 @@
function equals(a, b) {
return !!(EJSON.stringify(a) === EJSON.stringify(b));
}
Tinytest.add('cfs-collection-filters - client - test environment', function(test) {
test.isTrue(typeof FS.Collection !== 'undefined', 'test environment not initialized FS.Collection');
});
//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,27 @@
function equals(a, b) {
return !!(EJSON.stringify(a) === EJSON.stringify(b));
}
Tinytest.add('cfs-collection-filters - server - test environment', function(test) {
test.isTrue(typeof FS.Collection !== 'undefined', 'test environment not initialized FS.Collection');
});
//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,5 @@
language: node_js
node_js:
- "0.10"
before_install:
- "curl -L http://git.io/s0Zu-w | /bin/sh"

View file

@ -0,0 +1,727 @@
# Changelog
## vCurrent
## [v0.5.3] (https://github.com/zcfs/Meteor-CollectionFS/tree/v0.5.3)
#### 20/12/14 by Morten Henriksen
- add changelog
- Bump to version 0.5.3
## [v0.5.2] (https://github.com/zcfs/Meteor-CollectionFS/tree/v0.5.2)
#### 17/12/14 by Morten Henriksen
- Bump to version 0.5.2
## [v0.5.1] (https://github.com/zcfs/Meteor-CollectionFS/tree/v0.5.1)
#### 17/12/14 by Morten Henriksen
- mbr update, remove versions.json
- Bump to version 0.5.1
## [v0.5.0] (https://github.com/zcfs/Meteor-CollectionFS/tree/v0.5.0)
#### 17/12/14 by Morten Henriksen
- Merge branch 'devel' of https://github.com/zcfs/Meteor-CollectionFS into devel
- mbr update versions and fix warnings
- update pkg, dependencies, etc.
- Fix #483
- *Merged pull-request:* "mrt graphicsmagick instructions to meteor 0.9+" [#442](https://github.com/zcfs/Meteor-CollectionFS/issues/442) ([yogiben](https://github.com/yogiben))
- mrt graphicsmagick instructions to meteor 0.9+
- mention underlying collection
- *Merged pull-request:* "Added server-side Buffer example to README.md" [#405](https://github.com/zcfs/Meteor-CollectionFS/issues/405) ([abuddenb](https://github.com/abuddenb))
- change prop names
- Merge branch 'devel' of https://github.com/zcfs/Meteor-CollectionFS into devel
- add "Display an Uploaded Image" example
- addition to steps
- correct 0.9.0 steps
- Merge branch 'devel' of github.com:abuddenb/Meteor-CollectionFS into devel
- Added server-side Buffer example to README.md
Patches by GitHub users [@yogiben](https://github.com/yogiben), [@abuddenb](https://github.com/abuddenb).
## [v0.4.9] (https://github.com/zcfs/Meteor-CollectionFS/tree/v0.4.9)
#### 27/08/14 by Eric Dobbertin
- add 0.9.0 instructions
- change package name to lowercase
- Added server-side Buffer example to README.md
- *Merged pull-request:* "Updated README.md to reflect that FS.File - Objects can't be stored in t..." [#395](https://github.com/zcfs/Meteor-CollectionFS/issues/395) ([DanielDornhardt](https://github.com/DanielDornhardt))
- Updated README.md to reflect that FS.File - Objects can't be stored in the Server MongoDB at the moment.
- document key
- update the descriptions of collections
- document beforeWrite
- add download button example
- *Merged pull-request:* "Update README.md" [#354](https://github.com/zcfs/Meteor-CollectionFS/issues/354) ([karabijavad](https://github.com/karabijavad))
- should say to use cfs-graphicsmagick pkg
- fix typo
- *Merged pull-request:* "Added "Storing FS.File references in your objects" chapter to README" [#311](https://github.com/zcfs/Meteor-CollectionFS/issues/311) ([Sanjo](https://github.com/Sanjo))
- Added "Storing FS.File references in your objects" chapter to README
- a few more API updates
- updates for changed FS.File API
- additions and clarifications
- *Merged pull-request:* "Custom Metadata misleading example" [#282](https://github.com/zcfs/Meteor-CollectionFS/issues/282) ([czeslaaw](https://github.com/czeslaaw))
- Custom Metadata misleading example
Patches by GitHub users [@DanielDornhardt](https://github.com/DanielDornhardt), [@karabijavad](https://github.com/karabijavad), [@Sanjo](https://github.com/Sanjo), [@czeslaaw](https://github.com/czeslaaw).
## [v0.4.8] (https://github.com/zcfs/Meteor-CollectionFS/tree/v0.4.8)
#### 09/04/14 by Eric Dobbertin
- Add cfs-collection-filters pkg by default
- Use FS.Utility.setFileExtension in example
## [v0.4.7] (https://github.com/zcfs/Meteor-CollectionFS/tree/v0.4.7)
#### 05/04/14 by Morten Henriksen
- corrections, and move image examples here so we can deprecate cfs-imagemagick
## [v0.4.6] (https://github.com/zcfs/Meteor-CollectionFS/tree/v0.4.6)
#### 02/04/14 by Morten Henriksen
- *Fixed bug:* "Documentation Issues" [#241](https://github.com/zcfs/Meteor-CollectionFS/issues/241)
- point to cfs-graphicsmagick readme for transform examples
## [v0.4.5] (https://github.com/zcfs/Meteor-CollectionFS/tree/v0.4.5)
#### 31/03/14 by Eric Dobbertin
- use latest releases
- Add yet a note about mrt and collectionFS in the readme
## [v0.4.4] (https://github.com/zcfs/Meteor-CollectionFS/tree/v0.4.4)
#### 25/03/14 by Morten Henriksen
- attempt to get everything up to date
- Add notice about mrt having troubles figuring out deps
- *Merged pull-request:* "Removed redundant installation instructions." [#212](https://github.com/zcfs/Meteor-CollectionFS/issues/212) ([lleonard188](https://github.com/lleonard188))
- up to date
- read me update
- add test note
Patches by GitHub user [@lleonard188](https://github.com/lleonard188).
## [v0.4.3] (https://github.com/zcfs/Meteor-CollectionFS/tree/v0.4.3)
#### 23/03/14 by Morten Henriksen
- use collectionFS travis version force update
## [v0.4.2] (https://github.com/zcfs/Meteor-CollectionFS/tree/v0.4.2)
#### 22/03/14 by Morten Henriksen
- change deps
- Add read me about transformWrite
## [v0.4.1] (https://github.com/zcfs/Meteor-CollectionFS/tree/v0.4.1)
#### 21/03/14 by Morten Henriksen
- update reference to devel branch
- Merge branch 'devel' of https://github.com/zcfs/Meteor-CollectionFS into devel
- don't need to install transfer
- mark deprecated api docs
- test strikeout md
- no need to imply cfs-transfer since it doesn't do anything at the moment
- Remove ejson-file imply
## [v0.4.0] (https://github.com/zcfs/Meteor-CollectionFS/tree/v0.4.0)
#### 06/03/14 by Eric Dobbertin
- add section for documenting example code for common tasks, and add some of the examples; @raix feel free to add more
- add http upload package to smart.json
- Merge origin/devel into devel
- need to imply the http upload package because it's the default now
- update HTTP access point override docs
- update docs to use `devel` branch
- list component packages in smart.json
- Merge origin/devel into devel-merge
- add the ejson-file and correct doc
- refactor everything into packages
## [devel-merge-old] (https://github.com/zcfs/Meteor-CollectionFS/tree/devel-merge-old)
#### 12/02/14 by Eric Dobbertin
- update to latest FileSaver.js
- one of many refactores
- add optimization section
- add section explaining client vs server insert
- prevent autopublish for the SA collections
- remove arg that isn't used or passed
- changes to support client SA, plus clean/reorg SA code to have less duplication
- merge the concepts of "store" and "copy", update docs, switch to FS.Store namespace
- use wait:true to avoid Meteor issue
- extend allow when insecure package is installed
- document and improve code flow a bit
- faster to use onResultReceived callback
- use onload instead of onloadend because we want only successful loads
- remove some console logging
- fix issue where stuff wasn't uploading because we weren't calling getFileRecord() in the access point methods
- make allowed file extension checks not be case sensitive
- remove DDP "/del" access point since it's not used or necessary
- Remove collection-hooks dependency; use deny instead. Also some cleanup and code docs
- replace `mmmagic` dependency with a `mime` dependency; hopefully fixes issues we've seen with meteor deploy
- refactor to expose saveCopy to API; then change fileworker observes to be per-copy, calling saveCopy and allowing better control of which copies to create at which times
- use observes for all store saving and temp store deleting; add/adjust some api doc comments (didn't regenerate yet)
- improve fileIsAllowed check order and messages
- Add check for the Accounts package
- *Fixed bug:* "no userId passed to download allow validator" [#120](https://github.com/zcfs/Meteor-CollectionFS/issues/120)
- always append access token to url when a user is logged in; makes usage simpler to do opt-out rather than opt-in
- Don't queue ddp method calls
- Merge changes
- Optimize buffer handling
- Switch to MicroQueue
- improve memory management, attempt client side resume implementation, other minor fixes
- fix several issues, make downloading work, improve a few bits of code
- do the isImage test correctly; add some API docs
- isEmpty will be true for null or undefined
- rewrite getExtension so it works for unmounted files, too
- add strong reactive-list dependency for powerqueue
- minor fixes and code improvements; track uploading files by both collection and ID since one queue handles all collections
- Base the upload queue on PowerQueue sub queues
- Add sa note
- limit use of db
- Refactor and documentation
- use mmmagic 0.3.5
- *Fixed bug:* "Option to set Cache-Control and Max-Age" [#117](https://github.com/zcfs/Meteor-CollectionFS/issues/117)
- fix stuff that's broken
## [v0.3.7] (https://github.com/zcfs/Meteor-CollectionFS/tree/v0.3.7)
#### 08/01/14 by Morten Henriksen
- *Fixed bug:* "How to store files on the server side?" [#29](https://github.com/zcfs/Meteor-CollectionFS/issues/29)
- rework ejson and remove fsFile.reload
- explain complete upload process in ADVANCED docs
- Adds join to smart.json (its a weak dependency #119)
- link to api docs for acceptDropsOn
- typo/formatting fixes
- docs + insert callback returns `FS.File` instead of `id`
- Add docs and `FS.File.fetch`
- Add support for `Join`
- end with line
- smaller headlines in docs
- init api docs
- Refactor and documentation
- FS.Collection.insert should return the `FS.File` object
- remove some comments and such
- document custom connections
- add livedata ref to access DDP obj
- use separate ddp connection with option to pass in custom
- public folder and gm/im
- fix null options
- *Fixed bug:* "no userId passed to download allow validator" [#120](https://github.com/zcfs/Meteor-CollectionFS/issues/120)
- make useHTTP true by default
- skip auth checks if Package.insecure
- remove `callback` arg from fsFile.get since it's not used or necessary; fix partial gets such that they actually use getBytes, greatly speeding up downloads of large files
- reorg code and speed up downloads
- split TransferQueue into DownloadTransferQueue and UploadTransferQueue
- improvements to make use of new PowerQueue features
- fix issue with previous commit
- add accessPoints option
- Pull out temporary chunk code into a separate tempStore.js file, within a TempStore object. This makes it easier to maintain. Also updated the file worker code to correctly find temporary chunks that can be removed and delete those files.
- add security section
- Internally, change all "master" stuff to be the same as "copies". External API is still the same, but master options are copied to a special copy named "_master" so that all the other code can be cleaner. This may be a step toward being able to blur or eliminate the master/copy distinction, although there are still some benefits to having a master.
- Add instructions for installing for testing
- refactor code to be a bit cleaner
- add functions for getting an FS.File or setting FS.File data from a URL on the server
- clean up and improve some transfer code
- update console log message to be more correct
- ensure that fsFile.bytesUploaded is always set correctly
- fix some issues with recent commits
- *Merged pull-request:* "Updates the filter example area so that it works" [#109](https://github.com/zcfs/Meteor-CollectionFS/issues/109) ([cramhead](https://github.com/cramhead))
- add .npm to gitignore
- Remove .npm folder
- Clean clone just a bit
- Refactor fsCollection and argParser
- Comment on filter options
- Add download url
- refactor access point
- add put/get/del security based on allow/deny functions
- Updates the filter example area so that it works
- update for API change
- more client-side speed improvements and allow passing File/Blob to FS.File constructor again
- fix data mixup
- fix upload slowness and blocking
- change API to adjust issue with data loading callbacks
- revise FS.File API where data handling is concerned; fix some issues with callback handling in client-side methods; upshot should be faster, smoother uploads and downloads
- change names and put everything in exported FS namespace
- fix get/download of copies
- *Fixed bug:* "Files after certain size aren't saved properly. " [#104](https://github.com/zcfs/Meteor-CollectionFS/issues/104)
- add fileobject metadata and acceptDropsOn
- add some methods to load FO data from URL
- Correct allow/deny examples
- call put callback correctly
- add correct temporary installation instructions
- use correct filename when saving download
- filtering fixes
- removed some unused stuff
- adjust some comments
- switch api.remove to api.del for consistency with the other methods
- add hasCopy method
- new api; tons of changes
- use generic queue for server file handling
- update progress in the correct place
- minor changes to comments
- fix gridfs get method
- incorporate #82
- make filtering work (added collection-hooks dependency for core package)
- change UploadsCollection to CollectionFS; change former CollectionFS to GridFS and don't export it (used only by the gridfs storage adaptor); clean up some other areas and update readmes
- *Fixed bug:* "retrieveBlob failure" [#93](https://github.com/zcfs/Meteor-CollectionFS/issues/93)
- implement http methods URLs
- Merge branch 'devel' of https://github.com/zcfs/Meteor-CollectionFS.git into devel
- significant revisions to move downloading support to the UploadsCollection and make collectionFS/gridFS a pure storage adaptor
- Merge branch 'pr/94' into devel
- split and revise readmes
- rename packages and organize package.js
- error handling improvements
- better failure handling for removeCopy
- handlebars helper to display blob image in CFS package
- remove all encoding info
- refactor to fix multiple-file simultaneous uploads
- don't use _id in filesystem destination since it's not set anyway
- remove FileObject.file and instead save file as Blob (.blob) when FileObject.fromFile is called
- revise API a bit
- stop using strings and encoding and pass everything as Uint8Array (fixes downloading corruption)
- only attempt to delete file if it exists
- remove unused file
- complete refactoring; temporary for testing/tweaking and then will split into multiple packages
- *Merged pull-request:* "Implemented max parallel transfers" [#62](https://github.com/zcfs/Meteor-CollectionFS/issues/62) ([floo51](https://github.com/floo51))
Patches by GitHub users [@cramhead](https://github.com/cramhead), [@floo51](https://github.com/floo51).
## [v0.3.6] (https://github.com/zcfs/Meteor-CollectionFS/tree/v0.3.6)
#### 10/10/13 by Morten Henriksen
- Edit ideas about storage adapters and filehandlers
- Add MIT License
- Add paypal and weak deps
- Added the org. filemanager demo/example
## [v0.3.5] (https://github.com/zcfs/Meteor-CollectionFS/tree/v0.3.5)
#### 20/09/13 by Morten Henriksen
## [v0.3.4] (https://github.com/zcfs/Meteor-CollectionFS/tree/v0.3.4)
#### 20/09/13 by Morten Henriksen
- Extract the examples from collectionFS
- Added examples from @mxab
## [v0.3.3] (https://github.com/zcfs/Meteor-CollectionFS/tree/v0.3.3)
#### 18/09/13 by Morten Henriksen
- added travis badge
- Added travis badge
- Added basic environment
- Added metadata getter to docs
## [v0.3.2] (https://github.com/zcfs/Meteor-CollectionFS/tree/v0.3.2)
#### 17/09/13 by Morten Henriksen
## [v0.3.1] (https://github.com/zcfs/Meteor-CollectionFS/tree/v0.3.1)
#### 16/09/13 by Morten Henriksen
- Added some details about http.publishing of collections
- added check in filehandlers
- Updated som docs
- js hint added scope
- jshint clean up
- *Fixed bug:* "Exception from setTimeout callback: { stack: [Getter] }" [#64](https://github.com/zcfs/Meteor-CollectionFS/issues/64)
- Merge branch 'devel'
- Bump version to preview of 0.3.0
- Implemented max parallel transfers
- Revert "began refractoring"
- Revert "Run jshint through files"
- Revert "Preparing file object methods for better api"
- Revert "added fileobject files to package"
- added fileobject files to package
- Preparing file object methods for better api
- Run jshint through files
- *Merged pull-request:* "Improvements and Fixes to built-in helpers, storeFiles, and acceptDropsOn" [#51](https://github.com/zcfs/Meteor-CollectionFS/issues/51) ([aldeed](https://github.com/aldeed))
- Add generic events system, switch to "enums" for invalid event types, and change "fileFilter" to "filter" throughout. Update README to reflect these changes.
- Update readme to reflect storeFiles and acceptDropsOn changes, plus add documentation for all of the new built-in handlebars helpers
- commit some files that should be committed
- -Improve and fix built-in handlebars helpers -Prefix all built-in helpers with "cfs" -Update new example app to reflect helper changes, and improve and fix it a bit, too
- Merge remote-tracking branch 'upstream/master'
- update documentation of storeFiles and acceptDropsOn
- Added credit to @eprochasson
- *Merged pull-request:* "Should fix issue #45" [#50](https://github.com/zcfs/Meteor-CollectionFS/issues/50) ([eprochasson](https://github.com/eprochasson))
- *Fixed bug:* "file handlers not showing up in local demo" [#45](https://github.com/zcfs/Meteor-CollectionFS/issues/45)
- *Merged pull-request:* "Update README.md" [#49](https://github.com/zcfs/Meteor-CollectionFS/issues/49) ([eprochasson](https://github.com/eprochasson))
- Added meteor style guide for jshint
- fix package file
- fixes and improvements to storeFiles() and acceptDropsOn()
- Use a different saveAs shim
- Merge branch 'master' of https://github.com/aldeed/Meteor-CollectionFS
- Fixes and changes to support cfs changes
- -"cfs" prefix, new helpers, improvements and fixes -include saveAs shim in the package so that download button helper can reliably call it
- Add dependencies and files
- Document storeFiles() and acceptDropsOn()
- *Merged pull-request:* "Built-ins, fixes, etc." [#48](https://github.com/zcfs/Meteor-CollectionFS/issues/48) ([aldeed](https://github.com/aldeed))
- Add the new files to the package manifest
- Add underscore as dependency. It seems that Meteor may soon remove underscore from the core.
- Minor changes to support fileHanders() and fileFilter() function changes
- Copy in numeral.js for use by the built-in handlebar helper that displays file size in human readable format. This means the helper supports any of the format strings supported by numeral.js for file sizes.
- -Add fileFilter() function to specify allowed and disallowed files per collectionFS, based on extensions and/or content types -Add fileIsAllowed() function to easily check whether a particular file is allowed based on the rules set up by fileFilter() -Change all of the passthrough functions (find, findOne, update, remove, allow, deny) to pass through all function arguments more simply and more safely. This allows, for example, using find() instead of find({}). -Change fileHandlers() to extend the object whenever called, which means you can safely call it more than once -Add several utility functions for use in either client or server code
- -Add storeFiles API -Check that files are allowed by fileFilter before saving -Add acceptDropsOn API -Use .depend() instead of Deps throughout -Pull out _getProgress calc to use in two places -Add isUploading API -Improve isDownloading code
- New file to hold built-in handlebars helpers, including several initial helpers
- New file manager example app showing how to use new features, built-in handlebars helpers, etc.
- *Merged pull-request:* "Minor doc correction" [#47](https://github.com/zcfs/Meteor-CollectionFS/issues/47) ([aldeed](https://github.com/aldeed))
- *Merged pull-request:* "Documentation Improvement" [#46](https://github.com/zcfs/Meteor-CollectionFS/issues/46) ([aldeed](https://github.com/aldeed))
- Extensively revised README
Patches by GitHub users [@aldeed](https://github.com/aldeed), [@eprochasson](https://github.com/eprochasson).
## [v0.2.3] (https://github.com/zcfs/Meteor-CollectionFS/tree/v0.2.3)
#### 04/05/13 by Morten Henriksen
- *Fixed bug:* "Binary File Transfers Corrupted? Truncated?" [#41](https://github.com/zcfs/Meteor-CollectionFS/issues/41)
## [v0.2.2] (https://github.com/zcfs/Meteor-CollectionFS/tree/v0.2.2)
#### 03/05/13 by Morten Henriksen
- updated scope for serverConsole
- *Fixed bug:* "Error when using fresh meteor install" [#40](https://github.com/zcfs/Meteor-CollectionFS/issues/40)
- more text edits
- Minor text edits
- Add credit to README
- *Merged pull-request:* "Option for file encoding" [#36](https://github.com/zcfs/Meteor-CollectionFS/issues/36) ([nhibner](https://github.com/nhibner))
- Typo fix.
- Updated documentation (added encoding to the fileRecord structure).
- File encoding is stored in the fileRecord.
- Allow the user to specify an encoding for the buffer when storing on the server.
- *Merged pull-request:* "Fix backwards incompatibility" [#33](https://github.com/zcfs/Meteor-CollectionFS/issues/33) ([mitar](https://github.com/mitar))
- Fix backwards incompatibility.
Patches by GitHub users [@nhibner](https://github.com/nhibner), [@mitar](https://github.com/mitar).
## [v0.2.1] (https://github.com/zcfs/Meteor-CollectionFS/tree/v0.2.1)
#### 08/04/13 by Morten Henriksen
- Minor fixes, added dragndrop, minor refractoring
- Only work on completed files
## [v0.2.0] (https://github.com/zcfs/Meteor-CollectionFS/tree/v0.2.0)
#### 06/04/13 by Morten Henriksen
- Bump to 0.2.0 - Nice
- Big speed and refractoring
## [v0.1.9] (https://github.com/zcfs/Meteor-CollectionFS/tree/v0.1.9)
#### 31/03/13 by Morten Henriksen
- Added some more doc, deprecated autosubscribe to autopublish instead
## [v0.1.8] (https://github.com/zcfs/Meteor-CollectionFS/tree/v0.1.8)
#### 31/03/13 by Morten Henriksen
- Added maxFilehandlers to the doc
## [devel-#27-fixed] (https://github.com/zcfs/Meteor-CollectionFS/tree/devel-#27-fixed)
#### 31/03/13 by Morten Henriksen
- Added documentation by @petrocket
- Got filehandlers up and running in bundles
- Make a seperate thread + connection foreach collectionFS
## [v0.1.7] (https://github.com/zcfs/Meteor-CollectionFS/tree/v0.1.7)
#### 14/03/13 by Morten Henriksen
## [v0.1.6] (https://github.com/zcfs/Meteor-CollectionFS/tree/v0.1.6)
#### 13/01/13 by Morten Henriksen
- Converted .length to string to cope with Meteor use of underscore
## [devel-server-cache] (https://github.com/zcfs/Meteor-CollectionFS/tree/devel-server-cache)
#### 11/01/13 by Morten Henriksen
## [v0.1.5] (https://github.com/zcfs/Meteor-CollectionFS/tree/v0.1.5)
#### 11/01/13 by Morten Henriksen
- On going tests - db updates gone, requires refresh to commit changes to db - guess some que not working
- removed setTimeout when spawn == 1
## [v0.1.4] (https://github.com/zcfs/Meteor-CollectionFS/tree/v0.1.4)
#### 08/01/13 by Morten Henriksen
## [v0.1.3] (https://github.com/zcfs/Meteor-CollectionFS/tree/v0.1.3)
#### 08/01/13 by Morten Henriksen
## [v0.1.2] (https://github.com/zcfs/Meteor-CollectionFS/tree/v0.1.2)
#### 08/01/13 by Morten Henriksen
## [v0.1.1] (https://github.com/zcfs/Meteor-CollectionFS/tree/v0.1.1)
#### 08/01/13 by Morten Henriksen
- Big one, added fileHandler to create cashed versions of the file
- added made with Meteor in fileHandler example
- corrected param bug in find and findOne
- minor fix and update
- Corrected install guide to use Meteorite
## [v0.1.0] (https://github.com/zcfs/Meteor-CollectionFS/tree/v0.1.0)
#### 07/01/13 by Morten Henriksen
## [v0.1] (https://github.com/zcfs/Meteor-CollectionFS/tree/v0.1)
#### 07/01/13 by Morten Henriksen
- updated file strukture and example
- Restructure to package system
- Refractoring filenames and edit package.js
- Package.js added
- Added smart.json for Atmosphere packages - Not tested!
- Readme styling corrected
- Added comments to the upload, storeFile
- StoreFile should return fileId or null
- Added notes about security
- Added only owner can resume, makes sense for now
- Added how to make a download...
- Changed project title git
- And some more corrections
- More readme text
- Added some short reference
- some more md
- Initial 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,8 @@
wekan-cfs-collection
=========================
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.

View file

@ -0,0 +1,260 @@
/** @method FS.Collection.prototype.insert Insert `File` or `FS.File` or remote URL into collection
* @public
* @param {File|Blob|Buffer|ArrayBuffer|Uint8Array|String} fileRef File, FS.File, or other data to insert
* @param {function} [callback] Callback `function(error, fileObj)`
* @returns {FS.File|undefined} The `file object`
* [Meteor docs](http://docs.meteor.com/#insert)
*/
FS.Collection.prototype.insert = function(fileRef, callback) {
var self = this;
if (Meteor.isClient && !callback) {
callback = FS.Utility.defaultCallback;
}
// XXX:
// We should out factor beginStorage to FS.File.beginStorage
// the client side storage adapters should be the one providing
// the upload either via http/ddp or direct upload
// Could be cool to have a streaming api on the client side
// having a createReadStream etc. on the client too...
function beginStorage(fileObj) {
// If on client, begin uploading the data
if (Meteor.isClient) {
self.options.uploader && self.options.uploader(fileObj);
}
// If on the server, save the binary to a single chunk temp file,
// so that it is available when FileWorker calls saveCopies.
// This will also trigger file handling from collection observes.
else if (Meteor.isServer) {
fileObj.createReadStream().pipe(FS.TempStore.createWriteStream(fileObj));
}
}
// XXX: would be great if this function could be simplyfied - if even possible?
function checkAndInsert(fileObj) {
// Check filters. This is called in deny functions, too, but we call here to catch
// server inserts and to catch client inserts early, allowing us to call `onInvalid` on
// the client and save a trip to the server.
if (!self.allowsFile(fileObj)) {
return FS.Utility.handleError(callback, 'FS.Collection insert: file does not pass collection filters');
}
// Set collection name
fileObj.collectionName = self.name;
// Insert the file into db
// We call cloneFileRecord as an easy way of extracting the properties
// that need saving.
if (callback) {
fileObj._id = self.files.insert(FS.Utility.cloneFileRecord(fileObj), function(err, id) {
if (err) {
if (fileObj._id) {
delete fileObj._id;
}
} else {
// Set _id, just to be safe, since this could be before or after the insert method returns
fileObj._id = id;
// Pass to uploader or stream data to the temp store
beginStorage(fileObj);
}
callback(err, err ? void 0 : fileObj);
});
} else {
fileObj._id = self.files.insert(FS.Utility.cloneFileRecord(fileObj));
// Pass to uploader or stream data to the temp store
beginStorage(fileObj);
}
return fileObj;
}
// Parse, adjust fileRef
if (fileRef instanceof FS.File) {
return checkAndInsert(fileRef);
} else {
// For convenience, allow File, Blob, Buffer, data URI, filepath, URL, etc. to be passed as first arg,
// and we will attach that to a new fileobj for them
var fileObj = new FS.File(fileRef);
if (callback) {
fileObj.attachData(fileRef, function attachDataCallback(error) {
if (error) {
callback(error);
} else {
checkAndInsert(fileObj);
}
});
} else {
// We ensure there's a callback on the client, so if there isn't one at this point,
// we must be on the server expecting synchronous behavior.
fileObj.attachData(fileRef);
checkAndInsert(fileObj);
}
return fileObj;
}
};
/** @method FS.Collection.prototype.update Update the file record
* @public
* @param {FS.File|object} selector
* @param {object} modifier
* @param {object} [options]
* @param {function} [callback]
* [Meteor docs](http://docs.meteor.com/#update)
*/
FS.Collection.prototype.update = function(selector, modifier, options, callback) {
var self = this;
if (selector instanceof FS.File) {
// Make sure the file belongs to this FS.Collection
if (selector.collectionName === self.files._name) {
return selector.update(modifier, options, callback);
} else {
// Tried to save a file in the wrong FS.Collection
throw new Error('FS.Collection cannot update file belongs to: "' + selector.collectionName + '" not: "' + self.files._name + '"');
}
}
return self.files.update(selector, modifier, options, callback);
};
/** @method FS.Collection.prototype.remove Remove the file from the collection
* @public
* @param {FS.File|object} selector
* @param {Function} [callback]
* [Meteor docs](http://docs.meteor.com/#remove)
*/
FS.Collection.prototype.remove = function(selector, callback) {
var self = this;
if (selector instanceof FS.File) {
// Make sure the file belongs to this FS.Collection
if (selector.collectionName === self.files._name) {
return selector.remove(callback);
} else {
// Tried to remove a file from the wrong FS.Collection
throw new Error('FS.Collection cannot remove file belongs to: "' + selector.collectionName + '" not: "' + self.files._name + '"');
}
}
//doesn't work correctly on the client without a callback
callback = callback || FS.Utility.defaultCallback;
return self.files.remove(selector, callback);
};
/** @method FS.Collection.prototype.findOne
* @public
* @param {[selector](http://docs.meteor.com/#selectors)} selector
* [Meteor docs](http://docs.meteor.com/#findone)
* Example:
```js
var images = new FS.Collection( ... );
// Get the file object
var fo = images.findOne({ _id: 'NpnskCt6ippN6CgD8' });
```
*/
// Call findOne on files collection
FS.Collection.prototype.findOne = function(selector) {
var self = this;
return self.files.findOne.apply(self.files, arguments);
};
/** @method FS.Collection.prototype.find
* @public
* @param {[selector](http://docs.meteor.com/#selectors)} selector
* [Meteor docs](http://docs.meteor.com/#find)
* Example:
```js
var images = new FS.Collection( ... );
// Get the all file objects
var files = images.find({ _id: 'NpnskCt6ippN6CgD8' }).fetch();
```
*/
FS.Collection.prototype.find = function(selector) {
var self = this;
return self.files.find.apply(self.files, arguments);
};
/** @method FS.Collection.prototype.allow
* @public
* @param {object} options
* @param {function} options.download Function that checks if the file contents may be downloaded
* @param {function} options.insert
* @param {function} options.update
* @param {function} options.remove Functions that look at a proposed modification to the database and return true if it should be allowed
* @param {[string]} [options.fetch] Optional performance enhancement. Limits the fields that will be fetched from the database for inspection by your update and remove functions
* [Meteor docs](http://docs.meteor.com/#allow)
* Example:
```js
var images = new FS.Collection( ... );
// Get the all file objects
var files = images.allow({
insert: function(userId, doc) { return true; },
update: function(userId, doc, fields, modifier) { return true; },
remove: function(userId, doc) { return true; },
download: function(userId, fileObj) { return true; },
});
```
*/
FS.Collection.prototype.allow = function(options) {
var self = this;
// Pull out the custom "download" functions
if (options.download) {
if (!(options.download instanceof Function)) {
throw new Error("allow: Value for `download` must be a function");
}
self._validators.download.allow.push(options.download);
delete options.download;
}
return self.files.allow.call(self.files, options);
};
/** @method FS.Collection.prototype.deny
* @public
* @param {object} options
* @param {function} options.download Function that checks if the file contents may be downloaded
* @param {function} options.insert
* @param {function} options.update
* @param {function} options.remove Functions that look at a proposed modification to the database and return true if it should be denyed
* @param {[string]} [options.fetch] Optional performance enhancement. Limits the fields that will be fetched from the database for inspection by your update and remove functions
* [Meteor docs](http://docs.meteor.com/#deny)
* Example:
```js
var images = new FS.Collection( ... );
// Get the all file objects
var files = images.deny({
insert: function(userId, doc) { return true; },
update: function(userId, doc, fields, modifier) { return true; },
remove: function(userId, doc) { return true; },
download: function(userId, fileObj) { return true; },
});
```
*/
FS.Collection.prototype.deny = function(options) {
var self = this;
// Pull out the custom "download" functions
if (options.download) {
if (!(options.download instanceof Function)) {
throw new Error("deny: Value for `download` must be a function");
}
self._validators.download.deny.push(options.download);
delete options.download;
}
return self.files.deny.call(self.files, options);
};
// TODO: Upsert?
/**
* We provide a default implementation that doesn't do anything.
* Can be changed by user or packages, such as the default cfs-collection-filters pkg.
* @param {FS.File} fileObj File object
* @return {Boolean} Should we allow insertion of this file?
*/
FS.Collection.prototype.allowsFile = function fsColAllowsFile(fileObj) {
return true;
};

View file

@ -0,0 +1,180 @@
## cfs-collection Public API ##
CollectionFS, FS.Collection object
_API documentation automatically generated by [docmeteor](https://github.com/raix/docmeteor)._
-
### <a name="FS.Collection.prototype.insert"></a>*fsCollection*.insert(fileRef, [callback])&nbsp;&nbsp;<sub><i>Anywhere</i></sub> ###
*This method __insert__ is defined in `prototype` of `FS.Collection`*
__Arguments__
* __fileRef__ *{[FS.File](#FS.File)|[File](#File)}*
File data reference
* __callback__ *{function}* (Optional)
Callback `function(error, fileObj)`
__Returns__ *{FS.File}*
The `file object`
[Meteor docs](http://docs.meteor.com/#insert)
> ```FS.Collection.prototype.insert = function(fileRef, callback) { ...``` [api.common.js:8](api.common.js#L8)
-
### <a name="FS.Collection.prototype.update"></a>*fsCollection*.update(selector, modifier, [options], [callback])&nbsp;&nbsp;<sub><i>Anywhere</i></sub> ###
*This method __update__ is defined in `prototype` of `FS.Collection`*
__Arguments__
* __selector__ *{[FS.File](#FS.File)|object}*
* __modifier__ *{object}*
* __options__ *{object}* (Optional)
* __callback__ *{function}* (Optional)
[Meteor docs](http://docs.meteor.com/#update)
> ```FS.Collection.prototype.update = function(selector, modifier, options, callback) { ...``` [api.common.js:71](api.common.js#L71)
-
### <a name="FS.Collection.prototype.remove"></a>*fsCollection*.remove(selector, [callback])&nbsp;&nbsp;<sub><i>Anywhere</i></sub> ###
*This method __remove__ is defined in `prototype` of `FS.Collection`*
__Arguments__
* __selector__ *{[FS.File](#FS.File)|object}*
* __callback__ *{Function}* (Optional)
[Meteor docs](http://docs.meteor.com/#remove)
> ```FS.Collection.prototype.remove = function(selector, callback) { ...``` [api.common.js:92](api.common.js#L92)
-
### <a name="FS.Collection.prototype.findOne"></a>*fsCollection*.findOne(selector)&nbsp;&nbsp;<sub><i>Anywhere</i></sub> ###
*This method __findOne__ is defined in `prototype` of `FS.Collection`*
__Arguments__
* __selector__ *{[selector](http://docs.meteor.com/#selectors)}*
[Meteor docs](http://docs.meteor.com/#findone)
Example:
```js
var images = new FS.Collection( ... );
// Get the file object
var fo = images.findOne({ _id: 'NpnskCt6ippN6CgD8' });
```
> ```FS.Collection.prototype.findOne = function(selector) { ...``` [api.common.js:122](api.common.js#L122)
-
### <a name="FS.Collection.prototype.find"></a>*fsCollection*.find(selector)&nbsp;&nbsp;<sub><i>Anywhere</i></sub> ###
*This method __find__ is defined in `prototype` of `FS.Collection`*
__Arguments__
* __selector__ *{[selector](http://docs.meteor.com/#selectors)}*
[Meteor docs](http://docs.meteor.com/#find)
Example:
```js
var images = new FS.Collection( ... );
// Get the all file objects
var files = images.find({ _id: 'NpnskCt6ippN6CgD8' }).fetch();
```
> ```FS.Collection.prototype.find = function(selector) { ...``` [api.common.js:138](api.common.js#L138)
-
### <a name="FS.Collection.prototype.allow"></a>*fsCollection*.allow(options)&nbsp;&nbsp;<sub><i>Anywhere</i></sub> ###
*This method __allow__ is defined in `prototype` of `FS.Collection`*
__Arguments__
* __options__ *{object}*
* __download__ *{function}*
Function that checks if the file contents may be downloaded
* __insert__ *{function}*
* __update__ *{function}*
* __remove__ *{function}*
Functions that look at a proposed modification to the database and return true if it should be allowed
* __fetch__ *{[string]}* (Optional)
Optional performance enhancement. Limits the fields that will be fetched from the database for inspection by your update and remove functions
[Meteor docs](http://docs.meteor.com/#allow)
Example:
```js
var images = new FS.Collection( ... );
// Get the all file objects
var files = images.allow({
insert: function(userId, doc) { return true; },
update: function(userId, doc, fields, modifier) { return true; },
remove: function(userId, doc) { return true; },
download: function(userId, fileObj) { return true; },
});
```
> ```FS.Collection.prototype.allow = function(options) { ...``` [api.common.js:164](api.common.js#L164)
-
### <a name="FS.Collection.prototype.deny"></a>*fsCollection*.deny(options)&nbsp;&nbsp;<sub><i>Anywhere</i></sub> ###
*This method __deny__ is defined in `prototype` of `FS.Collection`*
__Arguments__
* __options__ *{object}*
* __download__ *{function}*
Function that checks if the file contents may be downloaded
* __insert__ *{function}*
* __update__ *{function}*
* __remove__ *{function}*
Functions that look at a proposed modification to the database and return true if it should be denyed
* __fetch__ *{[string]}* (Optional)
Optional performance enhancement. Limits the fields that will be fetched from the database for inspection by your update and remove functions
[Meteor docs](http://docs.meteor.com/#deny)
Example:
```js
var images = new FS.Collection( ... );
// Get the all file objects
var files = images.deny({
insert: function(userId, doc) { return true; },
update: function(userId, doc, fields, modifier) { return true; },
remove: function(userId, doc) { return true; },
download: function(userId, fileObj) { return true; },
});
```
> ```FS.Collection.prototype.deny = function(options) { ...``` [api.common.js:200](api.common.js#L200)

View file

@ -0,0 +1,171 @@
/**
*
* @constructor
* @param {string} name A name for the collection
* @param {Object} options
* @param {FS.StorageAdapter[]} options.stores An array of stores in which files should be saved. At least one is required.
* @param {Object} [options.filter] Filter definitions
* @param {Number} [options.chunkSize=2MB] Override the chunk size in bytes for uploads
* @param {Function} [options.uploader] A function to pass FS.File instances after inserting, which will begin uploading them. By default, `FS.HTTP.uploadQueue.uploadFile` is used if the `cfs-upload-http` package is present, or `FS.DDP.uploadQueue.uploadFile` is used if the `cfs-upload-ddp` package is present. You can override with your own, or set to `null` to prevent automatic uploading.
* @returns {undefined}
*/
FS.Collection = function(name, options) {
var self = this;
self.storesLookup = {};
self.primaryStore = {};
self.options = {
filter: null, //optional
stores: [], //required
chunkSize: null
};
// Define a default uploader based on which upload packages are present,
// preferring HTTP. You may override with your own function or
// set to null to skip automatic uploading of data after file insert/update.
if (FS.HTTP && FS.HTTP.uploadQueue) {
self.options.uploader = FS.HTTP.uploadQueue.uploadFile;
} else if (FS.DDP && FS.DDP.uploadQueue) {
self.options.uploader = FS.DDP.uploadQueue.uploadFile;
}
// Extend and overwrite options
FS.Utility.extend(self.options, options || {});
// Set the FS.Collection name
self.name = name;
// Make sure at least one store has been supplied.
// Usually the stores aren't used on the client, but we need them defined
// so that we can access their names and use the first one as the default.
if (FS.Utility.isEmpty(self.options.stores)) {
throw new Error("You must specify at least one store. Please consult the documentation.");
}
FS.Utility.each(self.options.stores, function(store, i) {
// Set the primary store
if (i === 0) {
self.primaryStore = store;
}
// Check for duplicate naming
if (typeof self.storesLookup[store.name] !== 'undefined') {
throw new Error('FS.Collection store names must be uniq, duplicate found: ' + store.name);
}
// Set the lookup
self.storesLookup[store.name] = store;
if (Meteor.isServer) {
// Emit events based on store events
store.on('stored', function (storeName, fileObj) {
// This is weird, but currently there is a bug where each store will emit the
// events for all other stores, too, so we need to make sure that this event
// is truly for this store.
if (storeName !== store.name)
return;
// When a file is successfully stored into the store, we emit a "stored" event on the FS.Collection only if the file belongs to this collection
if (fileObj.collectionName === name) {
var emitted = self.emit('stored', fileObj, store.name);
if (FS.debug && !emitted) {
console.log(fileObj.name({store: store.name}) + ' was successfully saved to the ' + store.name + ' store. You are seeing this informational message because you enabled debugging and you have not defined any listeners for the "stored" event on the ' + name + ' collection.');
}
}
fileObj.emit('stored', store.name);
});
store.on('error', function (storeName, error, fileObj) {
// This is weird, but currently there is a bug where each store will emit the
// events for all other stores, too, so we need to make sure that this event
// is truly for this store.
if (storeName !== store.name)
return;
// When a file has an error while being stored into the temp store, we emit an "error" event on the FS.Collection only if the file belongs to this collection
if (fileObj.collectionName === name) {
error = new Error('Error storing file to the ' + store.name + ' store: ' + error.message);
var emitted = self.emit('error', error, fileObj, store.name);
if (FS.debug && !emitted) {
console.log(error.message);
}
}
fileObj.emit('error', store.name);
});
}
});
var _filesOptions = {
transform: function(doc) {
// This should keep the filerecord in the file object updated in reactive
// context
var result = new FS.File(doc, true);
result.collectionName = name;
return result;
}
};
if(self.options.idGeneration) _filesOptions.idGeneration = self.options.idGeneration;
// Enable specifying an alternate driver, to change where the filerecord is stored
// Drivers can be created with MongoInternals.RemoteCollectionDriver()
if(self.options._driver){
_filesOptions._driver = self.options._driver;
}
// Create the 'cfs.' ++ ".filerecord" and use fsFile
var collectionName = 'cfs.' + name + '.filerecord';
self.files = new Mongo.Collection(collectionName, _filesOptions);
// For storing custom allow/deny functions
self._validators = {
download: {allow: [], deny: []}
};
// Set up filters
// XXX Should we deprecate the filter option now that this is done with a separate pkg, or just keep it?
if (self.filters) {
self.filters(self.options.filter);
}
// Save the collection reference (we want it without the 'cfs.' prefix and '.filerecord' suffix)
FS._collections[name] = this;
// Set up observers
Meteor.isServer && FS.FileWorker && FS.FileWorker.observe(this);
// Emit "removed" event on collection
self.files.find().observe({
removed: function(fileObj) {
self.emit('removed', fileObj);
}
});
// Emit events based on TempStore events
if (FS.TempStore) {
FS.TempStore.on('stored', function (fileObj, result) {
// When a file is successfully stored into the temp store, we emit an "uploaded" event on the FS.Collection only if the file belongs to this collection
if (fileObj.collectionName === name) {
var emitted = self.emit('uploaded', fileObj);
if (FS.debug && !emitted) {
console.log(fileObj.name() + ' was successfully uploaded. You are seeing this informational message because you enabled debugging and you have not defined any listeners for the "uploaded" event on the ' + name + ' collection.');
}
}
});
FS.TempStore.on('error', function (error, fileObj) {
// When a file has an error while being stored into the temp store, we emit an "error" event on the FS.Collection only if the file belongs to this collection
if (fileObj.collectionName === name) {
self.emit('error', new Error('Error storing uploaded file to TempStore: ' + error.message), fileObj);
}
});
} else if (Meteor.isServer) {
throw new Error("FS.Collection constructor: FS.TempStore must be defined before constructing any FS.Collections.")
}
};
// An FS.Collection can emit events
FS.Collection.prototype = new EventEmitter();

View file

@ -0,0 +1,390 @@
## Public and Private API ##
_API documentation automatically generated by [docmeteor](https://github.com/raix/docmeteor)._
***
__File: ["common.js"](common.js) Where: {client|server}__
***
#############################################################################
COLLECTION FS
#############################################################################
-
### <a name="FS.Collection"></a>new *fs*.Collection(name, options)&nbsp;&nbsp;<sub><i>Anywhere</i></sub> ###
*This method __Collection__ is defined in `FS`*
__Arguments__
* __name__ *{string}*
A name for the collection
* __options__ *{Object}*
* __stores__ *{[FS.StorageAdapter[]](#FS.StorageAdapter[])}*
An array of stores in which files should be saved. At least one is required.
* __filter__ *{Object}* (Optional)
Filter definitions
* __chunkSize__ *{Number}* (Optional, Default = 131072)
Override the chunk size in bytes for uploads and downloads
__Returns__ *{undefined}*
> ```FS.Collection = function(name, options) { ...``` [common.js:17](common.js#L17)
***
__File: ["api.common.js"](api.common.js) Where: {client|server}__
***
### <a name="FS.Collection.prototype.insert"></a>*fsCollection*.insert(fileRef, [callback])&nbsp;&nbsp;<sub><i>Anywhere</i></sub> ###
*This method __insert__ is defined in `prototype` of `FS.Collection`*
__Arguments__
* __fileRef__ *{[FS.File](#FS.File)|[File](#File)}*
File data reference
* __callback__ *{function}* (Optional)
Callback `function(error, fileObj)`
__Returns__ *{FS.File}*
The `file object`
[Meteor docs](http://docs.meteor.com/#insert)
> ```FS.Collection.prototype.insert = function(fileRef, callback) { ...``` [api.common.js:8](api.common.js#L8)
-
### <a name="FS.Collection.prototype.update"></a>*fsCollection*.update(selector, modifier, [options], [callback])&nbsp;&nbsp;<sub><i>Anywhere</i></sub> ###
*This method __update__ is defined in `prototype` of `FS.Collection`*
__Arguments__
* __selector__ *{[FS.File](#FS.File)|object}*
* __modifier__ *{object}*
* __options__ *{object}* (Optional)
* __callback__ *{function}* (Optional)
[Meteor docs](http://docs.meteor.com/#update)
> ```FS.Collection.prototype.update = function(selector, modifier, options, callback) { ...``` [api.common.js:71](api.common.js#L71)
-
### <a name="FS.Collection.prototype.remove"></a>*fsCollection*.remove(selector, [callback])&nbsp;&nbsp;<sub><i>Anywhere</i></sub> ###
*This method __remove__ is defined in `prototype` of `FS.Collection`*
__Arguments__
* __selector__ *{[FS.File](#FS.File)|object}*
* __callback__ *{Function}* (Optional)
[Meteor docs](http://docs.meteor.com/#remove)
> ```FS.Collection.prototype.remove = function(selector, callback) { ...``` [api.common.js:92](api.common.js#L92)
-
### <a name="FS.Collection.prototype.findOne"></a>*fsCollection*.findOne(selector)&nbsp;&nbsp;<sub><i>Anywhere</i></sub> ###
*This method __findOne__ is defined in `prototype` of `FS.Collection`*
__Arguments__
* __selector__ *{[selector](http://docs.meteor.com/#selectors)}*
[Meteor docs](http://docs.meteor.com/#findone)
Example:
```js
var images = new FS.Collection( ... );
// Get the file object
var fo = images.findOne({ _id: 'NpnskCt6ippN6CgD8' });
```
> ```FS.Collection.prototype.findOne = function(selector) { ...``` [api.common.js:122](api.common.js#L122)
-
### <a name="FS.Collection.prototype.find"></a>*fsCollection*.find(selector)&nbsp;&nbsp;<sub><i>Anywhere</i></sub> ###
*This method __find__ is defined in `prototype` of `FS.Collection`*
__Arguments__
* __selector__ *{[selector](http://docs.meteor.com/#selectors)}*
[Meteor docs](http://docs.meteor.com/#find)
Example:
```js
var images = new FS.Collection( ... );
// Get the all file objects
var files = images.find({ _id: 'NpnskCt6ippN6CgD8' }).fetch();
```
> ```FS.Collection.prototype.find = function(selector) { ...``` [api.common.js:138](api.common.js#L138)
-
### <a name="FS.Collection.prototype.allow"></a>*fsCollection*.allow(options)&nbsp;&nbsp;<sub><i>Anywhere</i></sub> ###
*This method __allow__ is defined in `prototype` of `FS.Collection`*
__Arguments__
* __options__ *{object}*
* __download__ *{function}*
Function that checks if the file contents may be downloaded
* __insert__ *{function}*
* __update__ *{function}*
* __remove__ *{function}*
Functions that look at a proposed modification to the database and return true if it should be allowed
* __fetch__ *{[string]}* (Optional)
Optional performance enhancement. Limits the fields that will be fetched from the database for inspection by your update and remove functions
[Meteor docs](http://docs.meteor.com/#allow)
Example:
```js
var images = new FS.Collection( ... );
// Get the all file objects
var files = images.allow({
insert: function(userId, doc) { return true; },
update: function(userId, doc, fields, modifier) { return true; },
remove: function(userId, doc) { return true; },
download: function(userId, fileObj) { return true; },
});
```
> ```FS.Collection.prototype.allow = function(options) { ...``` [api.common.js:164](api.common.js#L164)
-
### <a name="FS.Collection.prototype.deny"></a>*fsCollection*.deny(options)&nbsp;&nbsp;<sub><i>Anywhere</i></sub> ###
*This method __deny__ is defined in `prototype` of `FS.Collection`*
__Arguments__
* __options__ *{object}*
* __download__ *{function}*
Function that checks if the file contents may be downloaded
* __insert__ *{function}*
* __update__ *{function}*
* __remove__ *{function}*
Functions that look at a proposed modification to the database and return true if it should be denyed
* __fetch__ *{[string]}* (Optional)
Optional performance enhancement. Limits the fields that will be fetched from the database for inspection by your update and remove functions
[Meteor docs](http://docs.meteor.com/#deny)
Example:
```js
var images = new FS.Collection( ... );
// Get the all file objects
var files = images.deny({
insert: function(userId, doc) { return true; },
update: function(userId, doc, fields, modifier) { return true; },
remove: function(userId, doc) { return true; },
download: function(userId, fileObj) { return true; },
});
```
> ```FS.Collection.prototype.deny = function(options) { ...``` [api.common.js:200](api.common.js#L200)
***
__File: ["api.client.js"](api.client.js) Where: {client}__
***
### <a name="_eventCallback"></a>_eventCallback(templateName, selector, dataContext, evt, temp, fsFile)&nbsp;&nbsp;<sub><i>Client</i></sub> ###
*This method is private*
__Arguments__
* __templateName__ *{string}*
Name of template to apply events on
* __selector__ *{string}*
The element selector eg. "#uploadField"
* __dataContext__ *{object}*
The event datacontext
* __evt__ *{object}*
The event object { error, file }
* __temp__ *{object}*
The template instance
* __fsFile__ *{[FS.File](#FS.File)}*
File that triggered the event
> ```var _eventCallback = function fsEventCallback(templateName, selector, dataContext, evt, temp, fsFile) { ...``` [api.client.js:10](api.client.js#L10)
-
### <a name="_eachFile"></a>_eachFile(files, metadata, callback)&nbsp;&nbsp;<sub><i>Client</i></sub> ###
*This method is private*
__Arguments__
* __files__ *{array}*
List of files to iterate over
* __metadata__ *{object}*
Data to attach to the files
* __callback__ *{function}*
Function to pass the prepared `FS.File` object
> ```var _eachFile = function(files, metadata, callback) { ...``` [api.client.js:36](api.client.js#L36)
-
### <a name="FS.Collection.acceptDropsOn"></a>*fsCollection*.acceptDropsOn(templateName, selector, [metadata])&nbsp;&nbsp;<sub><i>Client</i></sub> ###
*This method __acceptDropsOn__ is defined in `FS.Collection`*
__Arguments__
* __templateName__ *{string}*
Name of template to apply events on
* __selector__ *{string}*
The element selector eg. "#uploadField"
* __metadata__ *{object|function}* (Optional)
Data/getter to attach to the file objects
Using this method adds an `uploaded` and `uploadFailed` event to the
template events. The event object contains `{ error, file }`
Example:
```css
.dropzone {
border: 2px dashed silver;
height: 5em;
padding-top: 3em;
-webkit-border-radius: 8px;
-moz-border-radius: 8px;
-ms-border-radius: 8px;
-o-border-radius: 8px;
border-radius: 8px;
}
```
```html
<template name="hello">
Choose file to upload:<br/>
<div id="dropzone" class="dropzone">
<div style="text-align: center; color: gray;">Drop file to upload</div>
</div>
</template>
```
```js
Template.hello.events({
'uploaded #dropzone': function(event, temp) {
console.log('Event Uploaded: ' + event.file._id);
}
});
images.acceptDropsOn('hello', '#dropzone');
```
> ```FS.Collection.prototype.acceptDropsOn = function(templateName, selector, metadata) { ...``` [api.client.js:99](api.client.js#L99)
-
### <a name="FS.Collection.acceptUploadFrom"></a>*fsCollection*.acceptUploadFrom(templateName, selector, [metadata])&nbsp;&nbsp;<sub><i>Client</i></sub> ###
*This method __acceptUploadFrom__ is defined in `FS.Collection`*
__Arguments__
* __templateName__ *{string}*
Name of template to apply events on
* __selector__ *{string}*
The element selector eg. "#uploadField"
* __metadata__ *{object|function}* (Optional)
Data/getter to attach to the file objects
Using this method adds an `uploaded` and `uploadFailed` event to the
template events. The event object contains `{ error, file }`
Example:
```html
<template name="hello">
Choose file to upload:<br/>
<input type="file" id="files" multiple/>
</template>
```
```js
Template.hello.events({
'uploaded #files': function(event, temp) {
console.log('Event Uploaded: ' + event.file._id);
}
});
images.acceptUploadFrom('hello', '#files');
```
> ```FS.Collection.prototype.acceptUploadFrom = function(templateName, selector, metadata) { ...``` [api.client.js:156](api.client.js#L156)

View file

@ -0,0 +1,43 @@
Package.describe({
name: 'wekan-cfs-collection',
version: '0.5.5',
summary: 'CollectionFS, FS.Collection object',
git: 'https://github.com/zcfs/Meteor-cfs-collection.git'
});
Package.onUse(function(api) {
api.versionsFrom('1.0');
api.use([
// CFS
'wekan-cfs-base-package@0.0.30',
'wekan-cfs-tempstore@0.1.4',
// Core
'deps',
'check',
'livedata',
'mongo-livedata',
// Other
'raix:eventemitter@0.1.1'
]);
// Weak dependencies for uploaders
api.use(['wekan-cfs-upload-http@0.0.20', 'wekan-cfs-upload-ddp@0.0.17'], { weak: true });
api.addFiles([
'common.js',
'api.common.js'
], 'client');
api.addFiles([
'common.js',
'api.common.js'
], 'server');
});
Package.onTest(function (api) {
api.use(['wekan-cfs-standard-packages', 'wekan-cfs-gridfs', 'tinytest', 'underscore', 'test-helpers']);
api.addFiles('tests/server-tests.js', 'server');
api.addFiles('tests/client-tests.js', 'client');
});

View file

@ -0,0 +1,33 @@
function equals(a, b) {
return !!(EJSON.stringify(a) === EJSON.stringify(b));
}
Tinytest.add('cfs-collection - test environment', function(test) {
test.isTrue(typeof FS.Collection !== 'undefined', 'test environment not initialized FS.Collection');
});
/*
* TODO FS.Collection Client Tests
*
*/
//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,96 @@
function equals(a, b) {
return !!(EJSON.stringify(a) === EJSON.stringify(b));
}
var fileCollection = new FS.Collection('files', {
stores: [
new FS.Store.GridFS('files')
]
});
Tinytest.add('cfs-collection - test environment', function(test) {
test.isTrue(typeof FS.Collection !== 'undefined', 'test environment not initialized FS.Collection');
});
Tinytest.add('cfs-collection - insert URL - sync - one', function (test) {
// XXX should switch to a local URL we host
// One
try {
fileCollection.insert("http://cdn.morguefile.com/imageData/public/files/b/bboomerindenial/preview/fldr_2009_04_01/file3301238617907.jpg");
test.isTrue(true);
} catch (err) {
test.isFalse(!!err, "URL insert failed");
}
});
Tinytest.add('cfs-collection - insert URL - sync - loop', function (test) {
// XXX should switch to a local URL we host
try {
for (var i = 0, len = 10; i < len; i++) {
fileCollection.insert("http://cdn.morguefile.com/imageData/public/files/b/bboomerindenial/preview/fldr_2009_04_01/file3301238617907.jpg");
}
test.isTrue(true);
} catch (err) {
test.isFalse(!!err, "URL insert failed");
}
});
Tinytest.addAsync('cfs-collection - insert URL - async - one', function (test, next) {
// XXX should switch to a local URL we host
// One
try {
fileCollection.insert("http://cdn.morguefile.com/imageData/public/files/b/bboomerindenial/preview/fldr_2009_04_01/file3301238617907.jpg", function (error, result) {
test.isNull(error);
test.instanceOf(result, FS.File);
next();
});
} catch (err) {
test.isFalse(!!err, "URL insert failed");
next();
}
});
Tinytest.addAsync('cfs-collection - insert URL - async - loop', function (test, next) {
// XXX should switch to a local URL we host
try {
var done = 0;
for (var i = 0, len = 10; i < len; i++) {
fileCollection.insert("http://cdn.morguefile.com/imageData/public/files/b/bboomerindenial/preview/fldr_2009_04_01/file3301238617907.jpg", function (error, result) {
test.isNull(error);
test.instanceOf(result, FS.File);
done++;
if (done === 10) {
next();
}
});
}
} catch (err) {
test.isFalse(!!err, "URL insert failed");
next();
}
});
//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,5 @@
language: node_js
node_js:
- "0.10"
before_install:
- "curl -L http://git.io/s0Zu-w | /bin/sh"

View file

@ -0,0 +1,20 @@
The MIT License (MIT)
Copyright (c) 2014 [@raix](https://github.com/raix), aka Morten N.O. Nørgaard Henriksen, mh@gi-software.com, and [@aldeed](https://github.com/aldeed), aka Eric Dobbertin
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,8 @@
wekan-cfs-data-man [![Build Status](https://travis-ci.org/CollectionFS/Meteor-data-man.svg?branch=master)](https://travis-ci.org/CollectionFS/Meteor-data-man)
=========================
Who can handle your arbitrary data? The DataMan can.
This is a package used by CollectionFS to attach data to file objects. Could be used for other things, though.
[Public API](api.md)

View file

@ -0,0 +1,309 @@
## data-man Public API ##
A data manager, allowing you to attach various types of data and get it back in various other types
_API documentation automatically generated by [docmeteor](https://github.com/raix/docmeteor)._
-
### <a name="DataMan"></a>new DataMan(data, [type])&nbsp;&nbsp;<sub><i>Client</i></sub> ###
__Arguments__
* __data__ *{[File](#File)|[Blob](#Blob)|ArrayBuffer|Uint8Array|String}*
The data that you want to manipulate.
* __type__ *{String}* (Optional)
The data content (MIME) type, if known. Required if the first argument is an ArrayBuffer, Uint8Array, or URL
> ```DataMan = function DataMan(data, type) { ...``` [client/data-man-api.js:8](client/data-man-api.js#L8)
-
### <a name="DataMan.prototype.getBlob"></a>*dataman*.getBlob(callback)&nbsp;&nbsp;<sub><i>Client</i></sub> ###
*This method __getBlob__ is defined in `prototype` of `DataMan`*
__Arguments__
* __callback__ *{Function}*
callback(error, blob)
__Returns__ *{undefined}*
Passes a Blob representing this data to a callback.
> ```DataMan.prototype.getBlob = function dataManGetBlob(callback) { ...``` [client/data-man-api.js:52](client/data-man-api.js#L52)
-
### <a name="DataMan.prototype.getBinary"></a>*dataman*.getBinary([start], [end], callback)&nbsp;&nbsp;<sub><i>Client</i></sub> ###
*This method __getBinary__ is defined in `prototype` of `DataMan`*
__Arguments__
* __start__ *{Number}* (Optional)
First byte position to read.
* __end__ *{Number}* (Optional)
Last byte position to read.
* __callback__ *{Function}*
callback(error, binaryData)
__Returns__ *{undefined}*
Passes a Uint8Array representing this data to a callback.
> ```DataMan.prototype.getBinary = function dataManGetBinary(start, end, callback) { ...``` [client/data-man-api.js:84](client/data-man-api.js#L84)
-
### <a name="DataMan.prototype.saveAs"></a>*dataman*.saveAs([filename])&nbsp;&nbsp;<sub><i>Client</i></sub> ###
*This method __saveAs__ is defined in `prototype` of `DataMan`*
__Arguments__
* __filename__ *{String}* (Optional)
__Returns__ *{undefined}*
Tells the browser to save the data like a normal downloaded file,
using the provided filename.
> ```DataMan.prototype.saveAs = function dataManSaveAs(filename) { ...``` [client/data-man-api.js:146](client/data-man-api.js#L146)
-
### <a name="DataMan.prototype.getDataUri"></a>*dataman*.getDataUri(callback)&nbsp;&nbsp;<sub><i>Client</i></sub> ###
*This method __getDataUri__ is defined in `prototype` of `DataMan`*
__Arguments__
* __callback__ *{function}*
callback(err, dataUri)
> ```DataMan.prototype.getDataUri = function dataManGetDataUri(callback) { ...``` [client/data-man-api.js:166](client/data-man-api.js#L166)
-
### <a name="DataMan.prototype.type"></a>*dataman*.type()&nbsp;&nbsp;<sub><i>Client</i></sub> ###
*This method __type__ is defined in `prototype` of `DataMan`*
Returns the type of the data.
> ```DataMan.prototype.type = function dataManType() { ...``` [client/data-man-api.js:227](client/data-man-api.js#L227)
-
### <a name="DataMan"></a>new DataMan(data, [type])&nbsp;&nbsp;<sub><i>Server</i></sub> ###
__Arguments__
* __data__ *{Buffer|ArrayBuffer|Uint8Array|String}*
The data that you want to manipulate.
* __type__ *{String}* (Optional)
The data content (MIME) type, if known. Required if the first argument is a Buffer, ArrayBuffer, Uint8Array, or URL
> ```DataMan = function DataMan(data, type) { ...``` [server/data-man-api.js:10](server/data-man-api.js#L10)
-
### <a name="DataMan.prototype.getBuffer"></a>*dataman*.getBuffer([callback])&nbsp;&nbsp;<sub><i>Server</i></sub> ###
*This method __getBuffer__ is defined in `prototype` of `DataMan`*
__Arguments__
* __callback__ *{function}* (Optional)
callback(err, buffer)
__Returns__ *{Buffer|undefined}*
Returns a Buffer representing this data, or passes the Buffer to a callback.
> ```DataMan.prototype.getBuffer = function dataManGetBuffer(callback) { ...``` [server/data-man-api.js:54](server/data-man-api.js#L54)
-
### <a name="DataMan.prototype.saveToFile"></a>*dataman*.saveToFile()&nbsp;&nbsp;<sub><i>Server</i></sub> ###
*This method __saveToFile__ is defined in `prototype` of `DataMan`*
__Returns__ *{undefined}*
Saves this data to a filepath on the local filesystem.
> ```DataMan.prototype.saveToFile = function dataManSaveToFile(filePath) { ...``` [server/data-man-api.js:66](server/data-man-api.js#L66)
-
### <a name="DataMan.prototype.getDataUri"></a>*dataman*.getDataUri([callback])&nbsp;&nbsp;<sub><i>Server</i></sub> ###
*This method __getDataUri__ is defined in `prototype` of `DataMan`*
__Arguments__
* __callback__ *{function}* (Optional)
callback(err, dataUri)
If no callback, returns the data URI.
> ```DataMan.prototype.getDataUri = function dataManGetDataUri(callback) { ...``` [server/data-man-api.js:84](server/data-man-api.js#L84)
-
### <a name="DataMan.prototype.createReadStream"></a>*dataman*.createReadStream()&nbsp;&nbsp;<sub><i>Server</i></sub> ###
*This method __createReadStream__ is defined in `prototype` of `DataMan`*
Returns a read stream for the data.
> ```DataMan.prototype.createReadStream = function dataManCreateReadStream() { ...``` [server/data-man-api.js:95](server/data-man-api.js#L95)
-
### <a name="DataMan.prototype.size"></a>*dataman*.size([callback])&nbsp;&nbsp;<sub><i>Server</i></sub> ###
*This method __size__ is defined in `prototype` of `DataMan`*
__Arguments__
* __callback__ *{function}* (Optional)
callback(err, size)
If no callback, returns the size in bytes of the data.
> ```DataMan.prototype.size = function dataManSize(callback) { ...``` [server/data-man-api.js:106](server/data-man-api.js#L106)
-
### <a name="DataMan.prototype.type"></a>*dataman*.type()&nbsp;&nbsp;<sub><i>Server</i></sub> ###
*This method __type__ is defined in `prototype` of `DataMan`*
Returns the type of the data.
> ```DataMan.prototype.type = function dataManType() { ...``` [server/data-man-api.js:117](server/data-man-api.js#L117)
-
### <a name="DataMan.Buffer"></a>new *dataman*.Buffer(buffer, type)&nbsp;&nbsp;<sub><i>Server</i></sub> ###
*This method __Buffer__ is defined in `DataMan`*
__Arguments__
* __buffer__ *{Buffer}*
* __type__ *{String}*
The data content (MIME) type.
> ```DataMan.Buffer = function DataManBuffer(buffer, type) { ...``` [server/data-man-buffer.js:10](server/data-man-buffer.js#L10)
-
### <a name="DataMan.DataURI"></a>new *dataman*.DataURI(dataUri)&nbsp;&nbsp;<sub><i>Server</i></sub> ###
*This method __DataURI__ is defined in `DataMan`*
__Arguments__
* __dataUri__ *{String}*
> ```DataMan.DataURI = function DataManDataURI(dataUri) { ...``` [server/data-man-datauri.js:7](server/data-man-datauri.js#L7)
-
### <a name="DataMan.FilePath"></a>new *dataman*.FilePath(filepath, [type])&nbsp;&nbsp;<sub><i>Server</i></sub> ###
*This method __FilePath__ is defined in `DataMan`*
__Arguments__
* __filepath__ *{String}*
* __type__ *{String}* (Optional)
The data content (MIME) type. Will lookup from file if not passed.
> ```DataMan.FilePath = function DataManFilePath(filepath, type) { ...``` [server/data-man-filepath.js:11](server/data-man-filepath.js#L11)
-
### <a name="DataMan.URL"></a>new *dataman*.URL(url, type)&nbsp;&nbsp;<sub><i>Server</i></sub> ###
*This method __URL__ is defined in `DataMan`*
__Arguments__
* __url__ *{String}*
* __type__ *{String}*
The data content (MIME) type.
> ```DataMan.URL = function DataManURL(url, type) { ...``` [server/data-man-url.js:10](server/data-man-url.js#L10)

View file

@ -0,0 +1,166 @@
/* Blob.js
* A Blob implementation.
* 2013-12-27
*
* By Eli Grey, http://eligrey.com
* By Devin Samarin, https://github.com/eboyjr
* License: X11/MIT
* See LICENSE.md
*/
/*global self, unescape */
/*jslint bitwise: true, regexp: true, confusion: true, es5: true, vars: true, white: true,
plusplus: true */
/*! @source http://purl.eligrey.com/github/Blob.js/blob/master/Blob.js */
if (!(typeof Blob === "function" || typeof Blob === "object") || typeof URL === "undefined")
if ((typeof Blob === "function" || typeof Blob === "object") && typeof webkitURL !== "undefined") self.URL = webkitURL;
else var Blob = (function (view) {
"use strict";
var BlobBuilder = view.BlobBuilder || view.WebKitBlobBuilder || view.MozBlobBuilder || view.MSBlobBuilder || (function(view) {
var
get_class = function(object) {
return Object.prototype.toString.call(object).match(/^\[object\s(.*)\]$/)[1];
}
, FakeBlobBuilder = function BlobBuilder() {
this.data = [];
}
, FakeBlob = function Blob(data, type, encoding) {
this.data = data;
this.size = data.length;
this.type = type;
this.encoding = encoding;
}
, FBB_proto = FakeBlobBuilder.prototype
, FB_proto = FakeBlob.prototype
, FileReaderSync = view.FileReaderSync
, FileException = function(type) {
this.code = this[this.name = type];
}
, file_ex_codes = (
"NOT_FOUND_ERR SECURITY_ERR ABORT_ERR NOT_READABLE_ERR ENCODING_ERR "
+ "NO_MODIFICATION_ALLOWED_ERR INVALID_STATE_ERR SYNTAX_ERR"
).split(" ")
, file_ex_code = file_ex_codes.length
, real_URL = view.URL || view.webkitURL || view
, real_create_object_URL = real_URL.createObjectURL
, real_revoke_object_URL = real_URL.revokeObjectURL
, URL = real_URL
, btoa = view.btoa
, atob = view.atob
, ArrayBuffer = view.ArrayBuffer
, Uint8Array = view.Uint8Array
;
FakeBlob.fake = FB_proto.fake = true;
while (file_ex_code--) {
FileException.prototype[file_ex_codes[file_ex_code]] = file_ex_code + 1;
}
if (!real_URL.createObjectURL) {
URL = view.URL = {};
}
URL.createObjectURL = function(blob) {
var
type = blob.type
, data_URI_header
;
if (type === null) {
type = "application/octet-stream";
}
if (blob instanceof FakeBlob) {
data_URI_header = "data:" + type;
if (blob.encoding === "base64") {
return data_URI_header + ";base64," + blob.data;
} else if (blob.encoding === "URI") {
return data_URI_header + "," + decodeURIComponent(blob.data);
} if (btoa) {
return data_URI_header + ";base64," + btoa(blob.data);
} else {
return data_URI_header + "," + encodeURIComponent(blob.data);
}
} else if (real_create_object_URL) {
return real_create_object_URL.call(real_URL, blob);
}
};
URL.revokeObjectURL = function(object_URL) {
if (object_URL.substring(0, 5) !== "data:" && real_revoke_object_URL) {
real_revoke_object_URL.call(real_URL, object_URL);
}
};
FBB_proto.append = function(data/*, endings*/) {
var bb = this.data;
// decode data to a binary string
if (Uint8Array && (data instanceof ArrayBuffer || data instanceof Uint8Array)) {
var
str = ""
, buf = new Uint8Array(data)
, i = 0
, buf_len = buf.length
;
for (; i < buf_len; i++) {
str += String.fromCharCode(buf[i]);
}
bb.push(str);
} else if (get_class(data) === "Blob" || get_class(data) === "File") {
if (FileReaderSync) {
var fr = new FileReaderSync;
bb.push(fr.readAsBinaryString(data));
} else {
// async FileReader won't work as BlobBuilder is sync
throw new FileException("NOT_READABLE_ERR");
}
} else if (data instanceof FakeBlob) {
if (data.encoding === "base64" && atob) {
bb.push(atob(data.data));
} else if (data.encoding === "URI") {
bb.push(decodeURIComponent(data.data));
} else if (data.encoding === "raw") {
bb.push(data.data);
}
} else {
if (typeof data !== "string") {
data += ""; // convert unsupported types to strings
}
// decode UTF-16 to binary string
bb.push(unescape(encodeURIComponent(data)));
}
};
FBB_proto.getBlob = function(type) {
if (!arguments.length) {
type = null;
}
return new FakeBlob(this.data.join(""), type, "raw");
};
FBB_proto.toString = function() {
return "[object BlobBuilder]";
};
FB_proto.slice = function(start, end, type) {
var args = arguments.length;
if (args < 3) {
type = null;
}
return new FakeBlob(
this.data.slice(start, args > 1 ? end : this.data.length)
, type
, this.encoding
);
};
FB_proto.toString = function() {
return "[object Blob]";
};
return FakeBlobBuilder;
}(view));
return function Blob(blobParts, options) {
var type = options ? (options.type || "") : "";
var builder = new BlobBuilder();
if (blobParts) {
for (var i = 0, len = blobParts.length; i < len; i++) {
builder.append(blobParts[i]);
}
}
return builder.getBlob(type);
};
}(typeof self !== "undefined" && self || typeof window !== "undefined" && window || this.content || this));

View file

@ -0,0 +1,302 @@
/**
* @method DataMan
* @public
* @constructor
* @param {File|Blob|ArrayBuffer|Uint8Array|String} data The data that you want to manipulate.
* @param {String} [type] The data content (MIME) type, if known. Required if the first argument is an ArrayBuffer, Uint8Array, or URL
*/
DataMan = function DataMan(data, type) {
var self = this;
if (!data) {
throw new Error("DataMan constructor requires a data argument");
}
// The end result of all this is that we will have one of the following set:
// - self.blob
// - self.url
// Unless we already have in-memory data, we don't load anything into memory
// and instead rely on obtaining a read stream when the time comes.
if (typeof File !== "undefined" && data instanceof File) {
self.blob = data; // File inherits from Blob so this is OK
self._type = data.type;
} else if (typeof Blob !== "undefined" && data instanceof Blob) {
self.blob = data;
self._type = data.type;
} else if (typeof ArrayBuffer !== "undefined" && data instanceof ArrayBuffer || EJSON.isBinary(data)) {
if (typeof Blob === "undefined") {
throw new Error("Browser must support Blobs to handle an ArrayBuffer or Uint8Array");
}
if (!type) {
throw new Error("DataMan constructor requires a type argument when passed an ArrayBuffer or Uint8Array");
}
self.blob = new Blob([data], {type: type});
self._type = type;
} else if (typeof data === "string") {
if (data.slice(0, 5) === "data:") {
self._type = data.slice(5, data.indexOf(';'));
self.blob = dataURItoBlob(data, self._type);
} else if (data.slice(0, 5) === "http:" || data.slice(0, 6) === "https:") {
if (!type) {
throw new Error("DataMan constructor requires a type argument when passed a URL");
}
self.url = data;
self._type = type;
} else {
throw new Error("DataMan constructor received unrecognized data string");
}
} else {
throw new Error("DataMan constructor received data that it doesn't support");
}
};
/**
* @method DataMan.prototype.getBlob
* @public
* @param {Function} [callback] - callback(error, blob)
* @returns {undefined|Blob}
*
* Passes a Blob representing this data to a callback or returns
* the Blob if no callback is provided. A callback is required
* if getting a Blob for a URL.
*/
DataMan.prototype.getBlob = function dataManGetBlob(callback) {
var self = this;
if (callback) {
if (self.blob) {
callback(null, self.blob);
} else if (self.url) {
var xhr = new XMLHttpRequest();
xhr.open('GET', self.url, true);
xhr.responseType = "blob";
xhr.onload = function(data) {
self.blob = xhr.response;
callback(null, self.blob);
};
xhr.onerror = function(err) {
callback(err);
};
xhr.send();
}
} else {
if (self.url)
throw new Error('DataMan.getBlob requires a callback when managing a URL');
return self.blob;
}
};
/**
* @method DataMan.prototype.getBinary
* @public
* @param {Number} [start] - First byte position to read.
* @param {Number} [end] - Last byte position to read.
* @param {Function} callback - callback(error, binaryData)
* @returns {undefined}
*
* Passes a Uint8Array representing this data to a callback.
*/
DataMan.prototype.getBinary = function dataManGetBinary(start, end, callback) {
var self = this;
if (typeof start === "function") {
callback = start;
}
callback = callback || defaultCallback;
function read(blob) {
if (typeof FileReader === "undefined") {
callback(new Error("Browser does not support FileReader"));
return;
}
var reader = new FileReader();
reader.onload = function(evt) {
callback(null, new Uint8Array(evt.target.result));
};
reader.onerror = function(err) {
callback(err);
};
reader.readAsArrayBuffer(blob);
}
self.getBlob(function (error, blob) {
if (error) {
callback(error);
} else {
if (typeof start === "number" && typeof end === "number") {
var size = blob.size;
// Return the requested chunk of binary data
if (start >= size) {
callback(new Error("DataMan.getBinary: start position beyond end of data (" + size + ")"));
return;
}
end = Math.min(size, end);
var slice = blob.slice || blob.webkitSlice || blob.mozSlice;
if (typeof slice === 'undefined') {
callback(new Error('Browser does not support File.slice'));
return;
}
read(slice.call(blob, start, end, self._type));
} else {
// Return the entire binary data
read(blob);
}
}
});
};
/** @method DataMan.prototype.saveAs
* @public
* @param {String} [filename]
* @return {undefined}
*
* Tells the browser to save the data like a normal downloaded file,
* using the provided filename.
*
*/
DataMan.prototype.saveAs = function dataManSaveAs(filename) {
var self = this;
if (typeof window === "undefined")
throw new Error("window must be defined to use saveLocal");
if (!window.saveAs) {
console.warn('DataMan.saveAs: window.saveAs not supported by this browser - add cfs-filesaver package');
return;
}
self.getBlob(function (error, blob) {
if (error) {
throw error;
} else {
window.saveAs(blob, filename);
}
});
};
/**
* @method DataMan.prototype.getDataUri
* @public
* @param {function} callback callback(err, dataUri)
*/
DataMan.prototype.getDataUri = function dataManGetDataUri(callback) {
// XXX: We could consider using: URL.createObjectURL(blob);
// This will create a reference to the blob data instead of a clone
// This is part of the File API - as the rest - Not sure how to generally
// support from IE10, FF26, Chrome 31, safari 7, opera 19, ios 6, android 4
var self = this;
if (typeof callback !== 'function')
throw new Error("getDataUri requires callback function");
if (typeof FileReader === "undefined") {
callback(new Error("Browser does not support FileReader"));
return;
}
var fileReader = new FileReader();
fileReader.onload = function(event) {
var dataUri = event.target.result;
callback(null, dataUri);
};
fileReader.onerror = function(err) {
callback(err);
};
self.getBlob(function (error, blob) {
if (error) {
callback(error);
} else {
fileReader.readAsDataURL(blob);
}
});
};
/**
* @method DataMan.prototype.size
* @public
* @param {function} [callback] callback(err, size)
*
* Passes the size of the data to the callback, if provided,
* or returns it. A callback is required to get the size of a URL on the client.
*/
DataMan.prototype.size = function dataManSize(callback) {
var self = this;
if (callback) {
if (typeof self._size === "number") {
callback(null, self._size);
} else {
self.getBlob(function (error, blob) {
if (error) {
callback(error);
} else {
self._size = blob.size;
callback(null, self._size);
}
});
}
} else {
if (self.url) {
throw new Error("On the client, DataMan.size requires a callback when getting size for a URL on the client");
} else if (typeof self._size === "number") {
return self._size;
} else {
var blob = self.getBlob();
self._size = blob.size;
return self._size;
}
}
};
/**
* @method DataMan.prototype.type
* @public
*
* Returns the type of the data.
*/
DataMan.prototype.type = function dataManType() {
return this._type;
};
/**
* @method dataURItoBlob
* @private
* @param {String} dataURI The data URI
* @param {String} dataTYPE The content type
* @returns {Blob} A new Blob instance
*
* Converts a data URI to a Blob.
*/
function dataURItoBlob(dataURI, dataTYPE) {
var str = atob(dataURI.split(',')[1]), array = [];
for(var i = 0; i < str.length; i++) array.push(str.charCodeAt(i));
return new Blob([new Uint8Array(array)], {type: dataTYPE});
}
/**
* @method defaultCallback
* @private
* @param {Error} [err]
* @returns {undefined}
*
* Can be used as a default callback for client methods that need a callback.
* Simply throws the provided error if there is one.
*/
function defaultCallback(err) {
if (err) {
// Show gentle error if Meteor error
if (err instanceof Meteor.Error) {
console.error(err.message);
} else {
// Normal error, just throw error
throw err;
}
}
}

View file

@ -0,0 +1,674 @@
## Public and Private API ##
_API documentation automatically generated by [docmeteor](https://github.com/raix/docmeteor)._
***
__File: ["client/Blob.js"](client/Blob.js) Where: {client}__
***
### <a name="if "></a>if {any}&nbsp;&nbsp;<sub><i>Client</i></sub> ###
```
Blob.js
A Blob implementation.
2013-06-20
By Eli Grey, http:
By Devin Samarin, https:
License: X11/MIT
See LICENSE.md
```
> ```if ((typeof Blob !== ``` [client/Blob.js:17](client/Blob.js#L17)
-
### <a name="if "></a>if {any}&nbsp;&nbsp;<sub><i>Client</i></sub> ###
```
global unescape jslint bitwise: true, regexp: true, confusion: true, es5: true, vars: true, white: true,
plusplus: true
```
> ```if ((typeof Blob !== ``` [client/Blob.js:17](client/Blob.js#L17)
***
__File: ["client/data-man-api.js"](client/data-man-api.js) Where: {client}__
***
### <a name="DataMan"></a>new DataMan(data, [type])&nbsp;&nbsp;<sub><i>Client</i></sub> ###
__Arguments__
* __data__ *{[File](#File)|[Blob](#Blob)|ArrayBuffer|Uint8Array|String}*
The data that you want to manipulate.
* __type__ *{String}* (Optional)
The data content (MIME) type, if known. Required if the first argument is an ArrayBuffer, Uint8Array, or URL
> ```DataMan = function DataMan(data, type) { ...``` [client/data-man-api.js:8](client/data-man-api.js#L8)
-
### <a name="DataMan.prototype.getBlob"></a>*dataman*.getBlob(callback)&nbsp;&nbsp;<sub><i>Client</i></sub> ###
*This method __getBlob__ is defined in `prototype` of `DataMan`*
__Arguments__
* __callback__ *{Function}*
callback(error, blob)
__Returns__ *{undefined}*
Passes a Blob representing this data to a callback.
> ```DataMan.prototype.getBlob = function dataManGetBlob(callback) { ...``` [client/data-man-api.js:52](client/data-man-api.js#L52)
-
### <a name="DataMan.prototype.getBinary"></a>*dataman*.getBinary([start], [end], callback)&nbsp;&nbsp;<sub><i>Client</i></sub> ###
*This method __getBinary__ is defined in `prototype` of `DataMan`*
__Arguments__
* __start__ *{Number}* (Optional)
First byte position to read.
* __end__ *{Number}* (Optional)
Last byte position to read.
* __callback__ *{Function}*
callback(error, binaryData)
__Returns__ *{undefined}*
Passes a Uint8Array representing this data to a callback.
> ```DataMan.prototype.getBinary = function dataManGetBinary(start, end, callback) { ...``` [client/data-man-api.js:84](client/data-man-api.js#L84)
-
### <a name="DataMan.prototype.saveAs"></a>*dataman*.saveAs([filename])&nbsp;&nbsp;<sub><i>Client</i></sub> ###
*This method __saveAs__ is defined in `prototype` of `DataMan`*
__Arguments__
* __filename__ *{String}* (Optional)
__Returns__ *{undefined}*
Tells the browser to save the data like a normal downloaded file,
using the provided filename.
> ```DataMan.prototype.saveAs = function dataManSaveAs(filename) { ...``` [client/data-man-api.js:146](client/data-man-api.js#L146)
-
### <a name="DataMan.prototype.getDataUri"></a>*dataman*.getDataUri(callback)&nbsp;&nbsp;<sub><i>Client</i></sub> ###
*This method __getDataUri__ is defined in `prototype` of `DataMan`*
__Arguments__
* __callback__ *{function}*
callback(err, dataUri)
> ```DataMan.prototype.getDataUri = function dataManGetDataUri(callback) { ...``` [client/data-man-api.js:166](client/data-man-api.js#L166)
-
### <a name="DataMan.prototype.type"></a>*dataman*.type()&nbsp;&nbsp;<sub><i>Client</i></sub> ###
*This method __type__ is defined in `prototype` of `DataMan`*
Returns the type of the data.
> ```DataMan.prototype.type = function dataManType() { ...``` [client/data-man-api.js:227](client/data-man-api.js#L227)
-
### <a name="dataURItoBlob"></a>dataURItoBlob(dataURI, dataTYPE)&nbsp;&nbsp;<sub><i>undefined</i></sub> ###
*This method is private*
__Arguments__
* __dataURI__ *{String}*
The data URI
* __dataTYPE__ *{String}*
The content type
__Returns__ *{Blob}*
A new Blob instance
Converts a data URI to a Blob.
> ```function dataURItoBlob(dataURI, dataTYPE) { ...``` [client/data-man-api.js:240](client/data-man-api.js#L240)
-
### <a name="defaultCallback"></a>defaultCallback([err])&nbsp;&nbsp;<sub><i>undefined</i></sub> ###
*This method is private*
__Arguments__
* __err__ *{[Error](#Error)}* (Optional)
__Returns__ *{undefined}*
Can be used as a default callback for client methods that need a callback.
Simply throws the provided error if there is one.
> ```function defaultCallback(err) { ...``` [client/data-man-api.js:255](client/data-man-api.js#L255)
***
__File: ["server/data-man-api.js"](server/data-man-api.js) Where: {server}__
***
### <a name="DataMan"></a>new DataMan(data, [type])&nbsp;&nbsp;<sub><i>Server</i></sub> ###
__Arguments__
* __data__ *{Buffer|ArrayBuffer|Uint8Array|String}*
The data that you want to manipulate.
* __type__ *{String}* (Optional)
The data content (MIME) type, if known. Required if the first argument is a Buffer, ArrayBuffer, Uint8Array, or URL
> ```DataMan = function DataMan(data, type) { ...``` [server/data-man-api.js:10](server/data-man-api.js#L10)
-
### <a name="DataMan.prototype.getBuffer"></a>*dataman*.getBuffer([callback])&nbsp;&nbsp;<sub><i>Server</i></sub> ###
*This method __getBuffer__ is defined in `prototype` of `DataMan`*
__Arguments__
* __callback__ *{function}* (Optional)
callback(err, buffer)
__Returns__ *{Buffer|undefined}*
Returns a Buffer representing this data, or passes the Buffer to a callback.
> ```DataMan.prototype.getBuffer = function dataManGetBuffer(callback) { ...``` [server/data-man-api.js:54](server/data-man-api.js#L54)
-
### <a name="DataMan.prototype.saveToFile"></a>*dataman*.saveToFile()&nbsp;&nbsp;<sub><i>Server</i></sub> ###
*This method __saveToFile__ is defined in `prototype` of `DataMan`*
__Returns__ *{undefined}*
Saves this data to a filepath on the local filesystem.
> ```DataMan.prototype.saveToFile = function dataManSaveToFile(filePath) { ...``` [server/data-man-api.js:66](server/data-man-api.js#L66)
-
### <a name="DataMan.prototype.getDataUri"></a>*dataman*.getDataUri([callback])&nbsp;&nbsp;<sub><i>Server</i></sub> ###
*This method __getDataUri__ is defined in `prototype` of `DataMan`*
__Arguments__
* __callback__ *{function}* (Optional)
callback(err, dataUri)
If no callback, returns the data URI.
> ```DataMan.prototype.getDataUri = function dataManGetDataUri(callback) { ...``` [server/data-man-api.js:84](server/data-man-api.js#L84)
-
### <a name="DataMan.prototype.createReadStream"></a>*dataman*.createReadStream()&nbsp;&nbsp;<sub><i>Server</i></sub> ###
*This method __createReadStream__ is defined in `prototype` of `DataMan`*
Returns a read stream for the data.
> ```DataMan.prototype.createReadStream = function dataManCreateReadStream() { ...``` [server/data-man-api.js:95](server/data-man-api.js#L95)
-
### <a name="DataMan.prototype.size"></a>*dataman*.size([callback])&nbsp;&nbsp;<sub><i>Server</i></sub> ###
*This method __size__ is defined in `prototype` of `DataMan`*
__Arguments__
* __callback__ *{function}* (Optional)
callback(err, size)
If no callback, returns the size in bytes of the data.
> ```DataMan.prototype.size = function dataManSize(callback) { ...``` [server/data-man-api.js:106](server/data-man-api.js#L106)
-
### <a name="DataMan.prototype.type"></a>*dataman*.type()&nbsp;&nbsp;<sub><i>Server</i></sub> ###
*This method __type__ is defined in `prototype` of `DataMan`*
Returns the type of the data.
> ```DataMan.prototype.type = function dataManType() { ...``` [server/data-man-api.js:117](server/data-man-api.js#L117)
***
__File: ["server/data-man-buffer.js"](server/data-man-buffer.js) Where: {server}__
***
### <a name="DataMan.Buffer"></a>new *dataman*.Buffer(buffer, type)&nbsp;&nbsp;<sub><i>Server</i></sub> ###
*This method __Buffer__ is defined in `DataMan`*
__Arguments__
* __buffer__ *{Buffer}*
* __type__ *{String}*
The data content (MIME) type.
> ```DataMan.Buffer = function DataManBuffer(buffer, type) { ...``` [server/data-man-buffer.js:10](server/data-man-buffer.js#L10)
-
### <a name="DataMan.Buffer.prototype.getBuffer"></a>*datamanBuffer*.getBuffer(callback)&nbsp;&nbsp;<sub><i>Server</i></sub> ###
*This method is private*
*This method __getBuffer__ is defined in `prototype` of `DataMan.Buffer`*
__Arguments__
* __callback__ *{function}*
callback(err, buffer)
__Returns__ *{Buffer|undefined}*
Passes a Buffer representing the data to a callback.
> ```DataMan.Buffer.prototype.getBuffer = function dataManBufferGetBuffer(callback) { ...``` [server/data-man-buffer.js:24](server/data-man-buffer.js#L24)
-
### <a name="DataMan.Buffer.prototype.getDataUri"></a>*datamanBuffer*.getDataUri(callback)&nbsp;&nbsp;<sub><i>Server</i></sub> ###
*This method is private*
*This method __getDataUri__ is defined in `prototype` of `DataMan.Buffer`*
__Arguments__
* __callback__ *{function}*
callback(err, dataUri)
Passes a data URI representing the data in the buffer to a callback.
> ```DataMan.Buffer.prototype.getDataUri = function dataManBufferGetDataUri(callback) { ...``` [server/data-man-buffer.js:35](server/data-man-buffer.js#L35)
-
### <a name="DataMan.Buffer.prototype.createReadStream"></a>*datamanBuffer*.createReadStream()&nbsp;&nbsp;<sub><i>Server</i></sub> ###
*This method is private*
*This method __createReadStream__ is defined in `prototype` of `DataMan.Buffer`*
Returns a read stream for the data.
> ```DataMan.Buffer.prototype.createReadStream = function dataManBufferCreateReadStream() { ...``` [server/data-man-buffer.js:51](server/data-man-buffer.js#L51)
-
### <a name="DataMan.Buffer.prototype.size"></a>*datamanBuffer*.size(callback)&nbsp;&nbsp;<sub><i>Server</i></sub> ###
*This method is private*
*This method __size__ is defined in `prototype` of `DataMan.Buffer`*
__Arguments__
* __callback__ *{function}*
callback(err, size)
Passes the size in bytes of the data in the buffer to a callback.
> ```DataMan.Buffer.prototype.size = function dataManBufferSize(callback) { ...``` [server/data-man-buffer.js:62](server/data-man-buffer.js#L62)
-
### <a name="DataMan.Buffer.prototype.type"></a>*datamanBuffer*.type()&nbsp;&nbsp;<sub><i>Server</i></sub> ###
*This method is private*
*This method __type__ is defined in `prototype` of `DataMan.Buffer`*
Returns the type of the data.
> ```DataMan.Buffer.prototype.type = function dataManBufferType() { ...``` [server/data-man-buffer.js:80](server/data-man-buffer.js#L80)
***
__File: ["server/data-man-datauri.js"](server/data-man-datauri.js) Where: {server}__
***
### <a name="DataMan.DataURI"></a>new *dataman*.DataURI(dataUri)&nbsp;&nbsp;<sub><i>Server</i></sub> ###
*This method __DataURI__ is defined in `DataMan`*
__Arguments__
* __dataUri__ *{String}*
> ```DataMan.DataURI = function DataManDataURI(dataUri) { ...``` [server/data-man-datauri.js:7](server/data-man-datauri.js#L7)
***
__File: ["server/data-man-filepath.js"](server/data-man-filepath.js) Where: {server}__
***
### <a name="DataMan.FilePath"></a>new *dataman*.FilePath(filepath, [type])&nbsp;&nbsp;<sub><i>Server</i></sub> ###
*This method __FilePath__ is defined in `DataMan`*
__Arguments__
* __filepath__ *{String}*
* __type__ *{String}* (Optional)
The data content (MIME) type. Will lookup from file if not passed.
> ```DataMan.FilePath = function DataManFilePath(filepath, type) { ...``` [server/data-man-filepath.js:11](server/data-man-filepath.js#L11)
-
### <a name="DataMan.FilePath.prototype.getBuffer"></a>*datamanFilepath*.getBuffer(callback)&nbsp;&nbsp;<sub><i>Server</i></sub> ###
*This method is private*
*This method __getBuffer__ is defined in `prototype` of `DataMan.FilePath`*
__Arguments__
* __callback__ *{function}*
callback(err, buffer)
__Returns__ *{Buffer|undefined}*
Passes a Buffer representing the data to a callback.
> ```DataMan.FilePath.prototype.getBuffer = function dataManFilePathGetBuffer(callback) { ...``` [server/data-man-filepath.js:25](server/data-man-filepath.js#L25)
-
### <a name="DataMan.FilePath.prototype.getDataUri"></a>*datamanFilepath*.getDataUri(callback)&nbsp;&nbsp;<sub><i>Server</i></sub> ###
*This method is private*
*This method __getDataUri__ is defined in `prototype` of `DataMan.FilePath`*
__Arguments__
* __callback__ *{function}*
callback(err, dataUri)
Passes a data URI representing the data to a callback.
> ```DataMan.FilePath.prototype.getDataUri = function dataManFilePathGetDataUri(callback) { ...``` [server/data-man-filepath.js:43](server/data-man-filepath.js#L43)
-
### <a name="DataMan.FilePath.prototype.createReadStream"></a>*datamanFilepath*.createReadStream()&nbsp;&nbsp;<sub><i>Server</i></sub> ###
*This method is private*
*This method __createReadStream__ is defined in `prototype` of `DataMan.FilePath`*
Returns a read stream for the data.
> ```DataMan.FilePath.prototype.createReadStream = function dataManFilePathCreateReadStream() { ...``` [server/data-man-filepath.js:67](server/data-man-filepath.js#L67)
-
### <a name="DataMan.FilePath.prototype.size"></a>*datamanFilepath*.size(callback)&nbsp;&nbsp;<sub><i>Server</i></sub> ###
*This method is private*
*This method __size__ is defined in `prototype` of `DataMan.FilePath`*
__Arguments__
* __callback__ *{function}*
callback(err, size)
Passes the size in bytes of the data to a callback.
> ```DataMan.FilePath.prototype.size = function dataManFilePathSize(callback) { ...``` [server/data-man-filepath.js:79](server/data-man-filepath.js#L79)
-
### <a name="DataMan.FilePath.prototype.type"></a>*datamanFilepath*.type()&nbsp;&nbsp;<sub><i>Server</i></sub> ###
*This method is private*
*This method __type__ is defined in `prototype` of `DataMan.FilePath`*
Returns the type of the data.
> ```DataMan.FilePath.prototype.type = function dataManFilePathType() { ...``` [server/data-man-filepath.js:104](server/data-man-filepath.js#L104)
***
__File: ["server/data-man-url.js"](server/data-man-url.js) Where: {server}__
***
### <a name="DataMan.URL"></a>new *dataman*.URL(url, type)&nbsp;&nbsp;<sub><i>Server</i></sub> ###
*This method __URL__ is defined in `DataMan`*
__Arguments__
* __url__ *{String}*
* __type__ *{String}*
The data content (MIME) type.
> ```DataMan.URL = function DataManURL(url, type) { ...``` [server/data-man-url.js:10](server/data-man-url.js#L10)
-
### <a name="DataMan.URL.prototype.getBuffer"></a>*datamanUrl*.getBuffer(callback)&nbsp;&nbsp;<sub><i>Server</i></sub> ###
*This method is private*
*This method __getBuffer__ is defined in `prototype` of `DataMan.URL`*
__Arguments__
* __callback__ *{function}*
callback(err, buffer)
__Returns__ *{Buffer|undefined}*
Passes a Buffer representing the data at the URL to a callback.
> ```DataMan.URL.prototype.getBuffer = function dataManUrlGetBuffer(callback) { ...``` [server/data-man-url.js:24](server/data-man-url.js#L24)
-
### <a name="DataMan.URL.prototype.getDataUri"></a>*datamanUrl*.getDataUri(callback)&nbsp;&nbsp;<sub><i>Server</i></sub> ###
*This method is private*
*This method __getDataUri__ is defined in `prototype` of `DataMan.URL`*
__Arguments__
* __callback__ *{function}*
callback(err, dataUri)
Passes a data URI representing the data at the URL to a callback.
> ```DataMan.URL.prototype.getDataUri = function dataManUrlGetDataUri(callback) { ...``` [server/data-man-url.js:57](server/data-man-url.js#L57)
-
### <a name="DataMan.URL.prototype.createReadStream"></a>*datamanUrl*.createReadStream()&nbsp;&nbsp;<sub><i>Server</i></sub> ###
*This method is private*
*This method __createReadStream__ is defined in `prototype` of `DataMan.URL`*
Returns a read stream for the data.
> ```DataMan.URL.prototype.createReadStream = function dataManUrlCreateReadStream() { ...``` [server/data-man-url.js:85](server/data-man-url.js#L85)
-
### <a name="DataMan.URL.prototype.size"></a>*datamanUrl*.size(callback)&nbsp;&nbsp;<sub><i>Server</i></sub> ###
*This method is private*
*This method __size__ is defined in `prototype` of `DataMan.URL`*
__Arguments__
* __callback__ *{function}*
callback(err, size)
Returns the size in bytes of the data at the URL.
> ```DataMan.URL.prototype.size = function dataManUrlSize(callback) { ...``` [server/data-man-url.js:97](server/data-man-url.js#L97)
-
### <a name="DataMan.URL.prototype.type"></a>*datamanUrl*.type()&nbsp;&nbsp;<sub><i>Server</i></sub> ###
*This method is private*
*This method __type__ is defined in `prototype` of `DataMan.URL`*
Returns the type of the data.
> ```DataMan.URL.prototype.type = function dataManUrlType() { ...``` [server/data-man-url.js:121](server/data-man-url.js#L121)

View file

@ -0,0 +1,48 @@
Package.describe({
name: 'wekan-cfs-data-man',
version: '0.0.6',
summary: 'A data manager, allowing you to attach various types of data and get it back in various other types',
git: 'https://github.com/zcfs/Meteor-data-man.git'
});
Npm.depends({
mime: "1.2.11",
'buffer-stream-reader': "0.1.1",
//request: "2.44.0",
// We use a specific commit from a fork of "request" package for now; we need fix for
// https://github.com/mikeal/request/issues/887 (https://github.com/zcfs/Meteor-CollectionFS/issues/347)
request: "https://github.com/wekan/request",
temp: "0.7.0" // for tests only
});
Package.onUse(function(api) {
api.versionsFrom('1.0');
api.use(['ejson']);
api.use(['wekan-cfs-filesaver@0.0.6'], {weak: true});
api.export('DataMan');
api.addFiles([
'client/Blob.js', //polyfill for browsers without Blob constructor; currently necessary for phantomjs support, too
'client/data-man-api.js'
], 'client');
api.addFiles([
'server/data-man-api.js',
'server/data-man-buffer.js',
'server/data-man-datauri.js',
'server/data-man-filepath.js',
'server/data-man-url.js',
'server/data-man-readstream.js'
], 'server');
});
Package.onTest(function (api) {
api.use(['wekan-cfs-data-man', 'http', 'tinytest', 'test-helpers', 'wekan-cfs-http-methods@0.0.29']);
api.addFiles(['tests/common.js', 'tests/client-tests.js'], 'client');
api.addFiles(['tests/common.js', 'tests/server-tests.js'], 'server');
});

View file

@ -0,0 +1,176 @@
/* global DataMan:true, Buffer */
var fs = Npm.require("fs");
var Readable = Npm.require('stream').Readable;
/**
* @method DataMan
* @public
* @constructor
* @param {Buffer|ArrayBuffer|Uint8Array|String} data The data that you want to manipulate.
* @param {String} [type] The data content (MIME) type, if known. Required if the first argument is a Buffer, ArrayBuffer, Uint8Array, or URL
* @param {Object} [options] Currently used only to pass options for the GET request when `data` is a URL.
*/
DataMan = function DataMan(data, type, options) {
var self = this, buffer;
if (!data) {
throw new Error("DataMan constructor requires a data argument");
}
// The end result of all this is that we will have this.source set to a correct
// data type handler. We are simply detecting what the data arg is.
//
// Unless we already have in-memory data, we don't load anything into memory
// and instead rely on obtaining a read stream when the time comes.
if (typeof Buffer !== "undefined" && data instanceof Buffer) {
if (!type) {
throw new Error("DataMan constructor requires a type argument when passed a Buffer");
}
self.source = new DataMan.Buffer(data, type);
} else if (typeof ArrayBuffer !== "undefined" && data instanceof ArrayBuffer) {
if (typeof Buffer === "undefined") {
throw new Error("Buffer support required to handle an ArrayBuffer");
}
if (!type) {
throw new Error("DataMan constructor requires a type argument when passed an ArrayBuffer");
}
buffer = new Buffer(new Uint8Array(data));
self.source = new DataMan.Buffer(buffer, type);
} else if (EJSON.isBinary(data)) {
if (typeof Buffer === "undefined") {
throw new Error("Buffer support required to handle an ArrayBuffer");
}
if (!type) {
throw new Error("DataMan constructor requires a type argument when passed a Uint8Array");
}
buffer = new Buffer(data);
self.source = new DataMan.Buffer(buffer, type);
} else if (typeof Readable !== "undefined" && data instanceof Readable) {
if (!type) {
throw new Error("DataMan constructor requires a type argument when passed a stream.Readable");
}
self.source = new DataMan.ReadStream(data, type);
} else if (typeof data === "string") {
if (data.slice(0, 5) === "data:") {
self.source = new DataMan.DataURI(data);
} else if (data.slice(0, 5) === "http:" || data.slice(0, 6) === "https:") {
if (!type) {
throw new Error("DataMan constructor requires a type argument when passed a URL");
}
self.source = new DataMan.URL(data, type, options);
} else {
// assume it's a filepath
self.source = new DataMan.FilePath(data, type);
}
} else {
throw new Error("DataMan constructor received data that it doesn't support");
}
};
/**
* @method DataMan.prototype.getBuffer
* @public
* @param {function} [callback] callback(err, buffer)
* @returns {Buffer|undefined}
*
* Returns a Buffer representing this data, or passes the Buffer to a callback.
*/
DataMan.prototype.getBuffer = function dataManGetBuffer(callback) {
var self = this;
return callback ? self.source.getBuffer(callback) : Meteor.wrapAsync(bind(self.source.getBuffer, self.source))();
};
function _saveToFile(readStream, filePath, callback) {
var writeStream = fs.createWriteStream(filePath);
writeStream.on('close', Meteor.bindEnvironment(function () {
callback();
}, function (error) { callback(error); }));
writeStream.on('error', Meteor.bindEnvironment(function (error) {
callback(error);
}, function (error) { callback(error); }));
readStream.pipe(writeStream);
}
/**
* @method DataMan.prototype.saveToFile
* @public
* @param {String} filePath
* @param {Function} callback
* @returns {undefined}
*
* Saves this data to a filepath on the local filesystem.
*/
DataMan.prototype.saveToFile = function dataManSaveToFile(filePath, callback) {
var readStream = this.createReadStream();
return callback ? _saveToFile(readStream, filePath, callback) : Meteor.wrapAsync(_saveToFile)(readStream, filePath);
};
/**
* @method DataMan.prototype.getDataUri
* @public
* @param {function} [callback] callback(err, dataUri)
*
* If no callback, returns the data URI.
*/
DataMan.prototype.getDataUri = function dataManGetDataUri(callback) {
var self = this;
return callback ? self.source.getDataUri(callback) : Meteor.wrapAsync(bind(self.source.getDataUri, self.source))();
};
/**
* @method DataMan.prototype.createReadStream
* @public
*
* Returns a read stream for the data.
*/
DataMan.prototype.createReadStream = function dataManCreateReadStream() {
return this.source.createReadStream();
};
/**
* @method DataMan.prototype.size
* @public
* @param {function} [callback] callback(err, size)
*
* If no callback, returns the size in bytes of the data.
*/
DataMan.prototype.size = function dataManSize(callback) {
var self = this;
return callback ? self.source.size(callback) : Meteor.wrapAsync(bind(self.source.size, self.source))();
};
/**
* @method DataMan.prototype.type
* @public
*
* Returns the type of the data.
*/
DataMan.prototype.type = function dataManType() {
return this.source.type();
};
/*
* "bind" shim; from underscorejs, but we avoid a dependency
*/
var slice = Array.prototype.slice;
var nativeBind = Function.prototype.bind;
var ctor = function(){};
function isFunction(obj) {
return Object.prototype.toString.call(obj) == '[object Function]';
}
function bind(func, context) {
var args, bound;
if (nativeBind && func.bind === nativeBind) return nativeBind.apply(func, slice.call(arguments, 1));
if (!isFunction(func)) throw new TypeError;
args = slice.call(arguments, 2);
return bound = function() {
if (!(this instanceof bound)) return func.apply(context, args.concat(slice.call(arguments)));
ctor.prototype = func.prototype;
var self = new ctor;
ctor.prototype = null;
var result = func.apply(self, args.concat(slice.call(arguments)));
if (Object(result) === result) return result;
return self;
};
}

View file

@ -0,0 +1,82 @@
var bufferStreamReader = Npm.require('buffer-stream-reader');
/**
* @method DataMan.Buffer
* @public
* @constructor
* @param {Buffer} buffer
* @param {String} type The data content (MIME) type.
*/
DataMan.Buffer = function DataManBuffer(buffer, type) {
var self = this;
self.buffer = buffer;
self._type = type;
};
/**
* @method DataMan.Buffer.prototype.getBuffer
* @private
* @param {function} callback callback(err, buffer)
* @returns {Buffer|undefined}
*
* Passes a Buffer representing the data to a callback.
*/
DataMan.Buffer.prototype.getBuffer = function dataManBufferGetBuffer(callback) {
callback(null, this.buffer);
};
/**
* @method DataMan.Buffer.prototype.getDataUri
* @private
* @param {function} callback callback(err, dataUri)
*
* Passes a data URI representing the data in the buffer to a callback.
*/
DataMan.Buffer.prototype.getDataUri = function dataManBufferGetDataUri(callback) {
var self = this;
if (!self._type) {
callback(new Error("DataMan.getDataUri couldn't get a contentType"));
} else {
var dataUri = "data:" + self._type + ";base64," + self.buffer.toString("base64");
callback(null, dataUri);
}
};
/**
* @method DataMan.Buffer.prototype.createReadStream
* @private
*
* Returns a read stream for the data.
*/
DataMan.Buffer.prototype.createReadStream = function dataManBufferCreateReadStream() {
return new bufferStreamReader(this.buffer);
};
/**
* @method DataMan.Buffer.prototype.size
* @param {function} callback callback(err, size)
* @private
*
* Passes the size in bytes of the data in the buffer to a callback.
*/
DataMan.Buffer.prototype.size = function dataManBufferSize(callback) {
var self = this;
if (typeof self._size === "number") {
callback(null, self._size);
return;
}
self._size = self.buffer.length;
callback(null, self._size);
};
/**
* @method DataMan.Buffer.prototype.type
* @private
*
* Returns the type of the data.
*/
DataMan.Buffer.prototype.type = function dataManBufferType() {
return this._type;
};

View file

@ -0,0 +1,14 @@
/**
* @method DataMan.DataURI
* @public
* @constructor
* @param {String} dataUri
*/
DataMan.DataURI = function DataManDataURI(dataUri) {
var self = this;
var pieces = dataUri.match(/^data:(.*);base64,(.*)$/);
var buffer = new Buffer(pieces[2], 'base64');
return new DataMan.Buffer(buffer, pieces[1]);
};
DataMan.DataURI.prototype = DataMan.Buffer.prototype;

View file

@ -0,0 +1,108 @@
var mime = Npm.require('mime');
var fs = Npm.require("fs");
/**
* @method DataMan.FilePath
* @public
* @constructor
* @param {String} filepath
* @param {String} [type] The data content (MIME) type. Will lookup from file if not passed.
*/
DataMan.FilePath = function DataManFilePath(filepath, type) {
var self = this;
self.filepath = filepath;
self._type = type || mime.lookup(filepath);
};
/**
* @method DataMan.FilePath.prototype.getBuffer
* @private
* @param {function} callback callback(err, buffer)
* @returns {Buffer|undefined}
*
* Passes a Buffer representing the data to a callback.
*/
DataMan.FilePath.prototype.getBuffer = function dataManFilePathGetBuffer(callback) {
var self = this;
// Call node readFile
fs.readFile(self.filepath, Meteor.bindEnvironment(function(err, buffer) {
callback(err, buffer);
}, function(err) {
callback(err);
}));
};
/**
* @method DataMan.FilePath.prototype.getDataUri
* @private
* @param {function} callback callback(err, dataUri)
*
* Passes a data URI representing the data to a callback.
*/
DataMan.FilePath.prototype.getDataUri = function dataManFilePathGetDataUri(callback) {
var self = this;
self.getBuffer(function (error, buffer) {
if (error) {
callback(error);
} else {
if (!self._type) {
callback(new Error("DataMan.getDataUri couldn't get a contentType"));
} else {
var dataUri = "data:" + self._type + ";base64," + buffer.toString("base64");
buffer = null;
callback(null, dataUri);
}
}
});
};
/**
* @method DataMan.FilePath.prototype.createReadStream
* @private
*
* Returns a read stream for the data.
*/
DataMan.FilePath.prototype.createReadStream = function dataManFilePathCreateReadStream() {
// Stream from filesystem
return fs.createReadStream(this.filepath);
};
/**
* @method DataMan.FilePath.prototype.size
* @param {function} callback callback(err, size)
* @private
*
* Passes the size in bytes of the data to a callback.
*/
DataMan.FilePath.prototype.size = function dataManFilePathSize(callback) {
var self = this;
if (typeof self._size === "number") {
callback(null, self._size);
return;
}
// We can get the size without buffering
fs.stat(self.filepath, Meteor.bindEnvironment(function (error, stats) {
if (stats && typeof stats.size === "number") {
self._size = stats.size;
callback(null, self._size);
} else {
callback(error);
}
}, function (error) {
callback(error);
}));
};
/**
* @method DataMan.FilePath.prototype.type
* @private
*
* Returns the type of the data.
*/
DataMan.FilePath.prototype.type = function dataManFilePathType() {
return this._type;
};

View file

@ -0,0 +1,80 @@
/* global DataMan */
var PassThrough = Npm.require('stream').PassThrough;
/**
* @method DataMan.ReadStream
* @public
* @constructor
* @param {ReadStream} stream
* @param {String} type The data content (MIME) type.
*/
DataMan.ReadStream = function DataManBuffer(stream, type) {
var self = this;
// Create a bufferable / paused new stream...
var pt = new PassThrough();
// Pipe provided read stream into pass-through stream
stream.pipe(pt);
// Set pass-through stream reference
self.stream = pt;
// Set type as provided
self._type = type;
};
/**
* @method DataMan.ReadStream.prototype.getBuffer
* @private
* @param {function} callback callback(err, buffer)
* @returns {undefined}
*
* Passes a Buffer representing the data to a callback.
*/
DataMan.ReadStream.prototype.getBuffer = function dataManReadStreamGetBuffer(/*callback*/) {
// TODO implement as passthrough stream?
};
/**
* @method DataMan.ReadStream.prototype.getDataUri
* @private
* @param {function} callback callback(err, dataUri)
*
* Passes a data URI representing the data in the stream to a callback.
*/
DataMan.ReadStream.prototype.getDataUri = function dataManReadStreamGetDataUri(/*callback*/) {
// TODO implement as passthrough stream?
};
/**
* @method DataMan.ReadStream.prototype.createReadStream
* @private
*
* Returns a read stream for the data.
*/
DataMan.ReadStream.prototype.createReadStream = function dataManReadStreamCreateReadStream() {
return this.stream;
};
/**
* @method DataMan.ReadStream.prototype.size
* @param {function} callback callback(err, size)
* @private
*
* Passes the size in bytes of the data in the stream to a callback.
*/
DataMan.ReadStream.prototype.size = function dataManReadStreamSize(callback) {
callback(0); // will determine from stream later
};
/**
* @method DataMan.ReadStream.prototype.type
* @private
*
* Returns the type of the data.
*/
DataMan.ReadStream.prototype.type = function dataManReadStreamType() {
return this._type;
};

View file

@ -0,0 +1,133 @@
var request = Npm.require("request");
/**
* @method DataMan.URL
* @public
* @constructor
* @param {String} url
* @param {String} type The data content (MIME) type.
*/
DataMan.URL = function DataManURL(url, type, options) {
var self = this;
options = options || {};
self.url = url;
self._type = type;
// This is some code borrowed from the http package. Hopefully
// we can eventually use HTTP pkg directly instead of 'request'
// once it supports streams and buffers and such. (`request` takes
// and `auth` option, too, but not of the same form as `HTTP`.)
if (options.auth) {
if (options.auth.indexOf(':') < 0)
throw new Error('auth option should be of the form "username:password"');
options.headers = options.headers || {};
options.headers['Authorization'] = "Basic "+
(new Buffer(options.auth, "ascii")).toString("base64");
delete options.auth;
}
self.urlOpts = options;
};
/**
* @method DataMan.URL.prototype.getBuffer
* @private
* @param {function} callback callback(err, buffer)
* @returns {Buffer|undefined}
*
* Passes a Buffer representing the data at the URL to a callback.
*/
DataMan.URL.prototype.getBuffer = function dataManUrlGetBuffer(callback) {
var self = this;
request(_.extend({
url: self.url,
method: "GET",
encoding: null,
jar: false
}, self.urlOpts), Meteor.bindEnvironment(function(err, res, body) {
if (err) {
callback(err);
} else {
self._type = res.headers['content-type'];
callback(null, body);
}
}, function(err) {
callback(err);
}));
};
/**
* @method DataMan.URL.prototype.getDataUri
* @private
* @param {function} callback callback(err, dataUri)
*
* Passes a data URI representing the data at the URL to a callback.
*/
DataMan.URL.prototype.getDataUri = function dataManUrlGetDataUri(callback) {
var self = this;
self.getBuffer(function (error, buffer) {
if (error) {
callback(error);
} else {
if (!self._type) {
callback(new Error("DataMan.getDataUri couldn't get a contentType"));
} else {
var dataUri = "data:" + self._type + ";base64," + buffer.toString("base64");
callback(null, dataUri);
}
}
});
};
/**
* @method DataMan.URL.prototype.createReadStream
* @private
*
* Returns a read stream for the data.
*/
DataMan.URL.prototype.createReadStream = function dataManUrlCreateReadStream() {
var self = this;
// Stream from URL
return request(_.extend({
url: self.url,
method: "GET"
}, self.urlOpts));
};
/**
* @method DataMan.URL.prototype.size
* @param {function} callback callback(err, size)
* @private
*
* Returns the size in bytes of the data at the URL.
*/
DataMan.URL.prototype.size = function dataManUrlSize(callback) {
var self = this;
if (typeof self._size === "number") {
callback(null, self._size);
return;
}
self.getBuffer(function (error, buffer) {
if (error) {
callback(error);
} else {
self._size = buffer.length;
callback(null, self._size);
}
});
};
/**
* @method DataMan.URL.prototype.type
* @private
*
* Returns the type of the data.
*/
DataMan.URL.prototype.type = function dataManUrlType() {
return this._type;
};

View file

@ -0,0 +1,296 @@
var blobData;
var arrayBufferData;
var binaryData;
var dataUriData;
var urlData;
// Init with Blob
Tinytest.addAsync('cfs-data - client - Init with Blob', function(test, onComplete) {
var blob = new Blob(['Hello World'], {type : 'text/plain'});
blobData = new DataMan(blob);
test.instanceOf(blobData.blob, Blob);
test.equal(blobData.type(), "text/plain");
onComplete();
});
// Init with ArrayBuffer
Tinytest.addAsync('cfs-data - client - Init with ArrayBuffer', function(test, onComplete) {
arrayBufferData = new DataMan(str2ab('Hello World'), "text/plain");
// Should be converted upon init to a Blob
test.instanceOf(arrayBufferData.blob, Blob);
test.equal(arrayBufferData.type(), "text/plain");
onComplete();
});
// Init with Binary
Tinytest.addAsync('cfs-data - client - Init with Binary', function(test, onComplete) {
binaryData = new DataMan(new Uint8Array(str2ab('Hello World')), "text/plain");
// Should be converted upon init to a Blob
test.instanceOf(arrayBufferData.blob, Blob);
test.equal(binaryData.type(), "text/plain");
onComplete();
});
// Init with data URI string
Tinytest.addAsync('cfs-data - client - Init with data URI string', function(test, onComplete) {
var dataUri = 'data:text/plain;base64,SGVsbG8gV29ybGQ='; //'Hello World'
dataUriData = new DataMan(dataUri);
// Should be converted upon init to a Blob
test.instanceOf(dataUriData.blob, Blob);
test.equal(dataUriData.type(), "text/plain"); //should be extracted from data URI
onComplete();
});
// Init with URL string
Tinytest.addAsync('cfs-data - client - Init with URL string', function(test, onComplete) {
urlData = new DataMan(Meteor.absoluteUrl('test'), "text/plain"); //'Hello World'
// URLs are not converted to Blobs upon init
test.equal(urlData.url, Meteor.absoluteUrl('test'));
test.equal(urlData.type(), "text/plain");
onComplete();
});
// getBlob
Tinytest.addAsync('cfs-data - client - getBlob', function(test, onComplete) {
var total = 10, done = 0;
function continueIfDone() {
done++;
if (total === done) {
onComplete();
}
}
function testBlob(error, blob, testType) {
test.isFalse(!!error, testType + ' got error: ' + (error && error.message));
test.instanceOf(blob, Blob, testType + ' got no blob');
if (blob instanceof Blob) {
var reader = new FileReader();
reader.addEventListener("load", function(event) {
test.equal(reader.result, 'Hello World', testType + ' got back blob with incorrect data');
continueIfDone();
}, false);
reader.addEventListener("error", function(err) {
test.equal(reader.error, null, testType + ' error reading blob as text');
continueIfDone();
}, false);
reader.readAsText(blob, 'utf-8');
} else {
continueIfDone();
}
}
// from Blob
blobData.getBlob(function (error, blob) {
testBlob(error, blob, 'getBlob from Blob');
});
// from Blob (no callback)
testBlob(false, blobData.getBlob(), 'getBlob from Blob');
// from ArrayBuffer
arrayBufferData.getBlob(function (error, blob) {
testBlob(error, blob, 'getBlob from ArrayBuffer');
});
// from ArrayBuffer (no callback)
testBlob(false, arrayBufferData.getBlob(), 'getBlob from ArrayBuffer');
// from binary
binaryData.getBlob(function (error, blob) {
testBlob(error, blob, 'getBlob from binary');
});
// from binary (no callback)
testBlob(false, binaryData.getBlob(), 'getBlob from binary');
// from data URI
dataUriData.getBlob(function (error, blob) {
testBlob(error, blob, 'getBlob from data URI');
});
// from data URI (no callback)
testBlob(false, dataUriData.getBlob(), 'getBlob from data URI');
// from URL
urlData.getBlob(function (error, blob) {
testBlob(error, blob, 'getBlob from URL');
});
// from URL (no callback)
test.throws(function () {
// callback is required for URLs on the client
urlData.getBlob();
});
continueIfDone();
});
// getBinary
Tinytest.addAsync('cfs-data - client - getBinary', function(test, onComplete) {
var total = 5, done = 0;
function continueIfDone() {
done++;
if (total === done) {
onComplete();
}
}
function testBinary(error, binary, testType) {
test.isFalse(!!error, testType + ' got error: ' + (error && error.message));
test.isTrue(EJSON.isBinary(binary), testType + ' got no binary');
if (EJSON.isBinary(binary)) {
test.equal(bin2str(binary), 'Hello World', testType + ' got back binary with incorrect data');
continueIfDone();
} else {
continueIfDone();
}
}
// from Blob
blobData.getBinary(function (error, binary) {
testBinary(error, binary, 'getBinary from Blob');
});
// from ArrayBuffer
arrayBufferData.getBinary(function (error, binary) {
testBinary(error, binary, 'getBinary from ArrayBuffer');
});
// from binary
binaryData.getBinary(function (error, binary) {
testBinary(error, binary, 'getBinary from binary');
});
// from data URI
dataUriData.getBinary(function (error, binary) {
testBinary(error, binary, 'getBinary from data URI');
});
// from URL
urlData.getBinary(function (error, binary) {
testBinary(error, binary, 'getBinary from URL');
});
});
// getDataUri
Tinytest.addAsync('cfs-data - client - getDataUri', function(test, onComplete) {
var total = 5, done = 0;
function testURI(error, uri, testType) {
test.isFalse(!!error, testType + ' got error: ' + (error && error.message));
test.equal(typeof uri, "string", testType + ' got no URI string');
test.equal(uri, 'data:text/plain;base64,SGVsbG8gV29ybGQ=', testType + ' got invalid URI');
done++;
if (total === done) {
onComplete();
}
}
// from Blob
blobData.getDataUri(function (error, uri) {
testURI(error, uri, 'getDataUri from Blob');
});
// from ArrayBuffer
arrayBufferData.getDataUri(function (error, uri) {
testURI(error, uri, 'getDataUri from ArrayBuffer');
});
// from binary
binaryData.getDataUri(function (error, uri) {
testURI(error, uri, 'getDataUri from binary');
});
// from data URI
dataUriData.getDataUri(function (error, uri) {
testURI(error, uri, 'getDataUri from data URI');
});
// from URL
urlData.getDataUri(function (error, uri) {
testURI(error, uri, 'getDataUri from URL');
});
});
// size
Tinytest.addAsync('cfs-data - client - size', function(test, onComplete) {
var total = 10, done = 0;
function continueIfDone() {
done++;
if (total === done) {
onComplete();
}
}
function testSize(error, size, testType) {
test.isFalse(!!error, testType + ' got error: ' + (error && error.message));
test.equal(size, 11, testType + ' got wrong size');
continueIfDone();
}
// from Blob
blobData.size(function (error, size) {
testSize(error, size, 'size from Blob');
});
// from Blob (no callback)
testSize(false, blobData.size(), 'size from Blob');
// from ArrayBuffer
arrayBufferData.size(function (error, size) {
testSize(error, size, 'size from ArrayBuffer');
});
// from ArrayBuffer (no callback)
testSize(false, arrayBufferData.size(), 'size from ArrayBuffer');
// from binary
binaryData.size(function (error, size) {
testSize(error, size, 'size from binary');
});
// from binary (no callback)
testSize(false, binaryData.size(), 'size from binary');
// from data URI
dataUriData.size(function (error, size) {
testSize(error, size, 'size from data URI');
});
// from data URI (no callback)
testSize(false, dataUriData.size(), 'size from data URI');
// from URL
urlData.size(function (error, size) {
testSize(error, size, 'size from URL');
});
// from URL (no callback)
test.throws(function () {
// callback is required for URLs on the client
urlData.size();
});
continueIfDone();
});
//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,38 @@
// ab2str = function ab2str(buf) {
// return String.fromCharCode(new Uint8Array(buf));
// }
bin2str = function bin2str(bufView) {
var length = bufView.length;
var result = '';
for (var i = 0; i<length; i+=65535) {
var addition = 65535;
if(i + 65535 > length) {
addition = length - i;
}
try {
// this fails on phantomjs due to old webkit bug; hence the try/catch
result += String.fromCharCode.apply(null, bufView.subarray(i,i+addition));
} catch (e) {
var dataArray = [];
for (var j = i; j < i+addition; j++) {
dataArray.push(bufView[j]);
}
result += String.fromCharCode.apply(null, dataArray);
}
}
return result;
};
ab2str = function ab2str(buffer) {
return bin2str(new Uint8Array(buffer));
};
str2ab = function str2ab(str) {
var buf = new ArrayBuffer(str.length);
var bufView = new Uint8Array(buf);
for (var i=0, strLen=str.length; i<strLen; i++) {
bufView[i] = str.charCodeAt(i);
}
return buf;
};

View file

@ -0,0 +1,366 @@
var fs = Npm.require('fs');
var temp = Npm.require('temp');
// Automatically track and cleanup files at exit
temp.track();
// Set up HTTP method URL used by client tests
HTTP.methods({
'test': {
get: function () {
var buf = new Buffer('Hello World');
this.setContentType('text/plain');
return buf;
},
head: function () {
var buf = new Buffer('Hello World');
this.setContentType('text/plain');
this.addHeader('Content-Length', buf.length);
buf = null;
}
}
});
// Save temp file for testing with
function openTempFile(name, callback) {
return temp.open(name, callback);
}
var openTempFileSync = Meteor.wrapAsync(openTempFile);
var info = openTempFileSync(null);
var tempFilePath = info.path;
fs.writeSync(info.fd, 'Hello World');
fs.closeSync(info.fd);
var bufferData;
var arrayBufferData;
var binaryData;
var dataUriData;
var urlData;
var filePathData;
var streamData;
// Init with Buffer
Tinytest.addAsync('cfs-data - server - Init with Buffer', function(test, onComplete) {
bufferData = new DataMan(new Buffer('Hello World'), "text/plain");
test.instanceOf(bufferData.source, DataMan.Buffer);
test.equal(bufferData.type(), "text/plain");
onComplete();
});
// Init with ArrayBuffer
Tinytest.addAsync('cfs-data - server - Init with ArrayBuffer', function(test, onComplete) {
arrayBufferData = new DataMan(str2ab('Hello World'), "text/plain");
// Should be converted upon init to a Buffer
test.instanceOf(arrayBufferData.source, DataMan.Buffer);
test.equal(arrayBufferData.type(), "text/plain");
onComplete();
});
// Init with Binary
Tinytest.addAsync('cfs-data - server - Init with Binary', function(test, onComplete) {
binaryData = new DataMan(new Uint8Array(str2ab('Hello World')), "text/plain");
// Should be converted upon init to a Buffer
test.instanceOf(arrayBufferData.source, DataMan.Buffer);
test.equal(binaryData.type(), "text/plain");
onComplete();
});
// Init with data URI string
Tinytest.addAsync('cfs-data - server - Init with data URI string', function(test, onComplete) {
var dataUri = 'data:text/plain;base64,SGVsbG8gV29ybGQ='; //'Hello World'
dataUriData = new DataMan(dataUri);
// Data URIs are not converted to Buffers upon init
test.instanceOf(dataUriData.source, DataMan.DataURI);
test.equal(dataUriData.type(), "text/plain"); //should be extracted from data URI
onComplete();
});
// Init with URL string
Tinytest.addAsync('cfs-data - server - Init with URL string', function(test, onComplete) {
var url = Meteor.absoluteUrl('test');
urlData = new DataMan(url, "text/plain"); //'Hello World'
// URLs are not converted to Buffers upon init
test.instanceOf(urlData.source, DataMan.URL);
test.equal(urlData.type(), "text/plain");
onComplete();
});
// Init with filepath string
Tinytest.addAsync('cfs-data - server - Init with filepath string', function(test, onComplete) {
filePathData = new DataMan(tempFilePath, "text/plain");
// filepaths are not converted to Buffers upon init
test.instanceOf(filePathData.source, DataMan.FilePath);
test.equal(filePathData.type(), "text/plain");
onComplete();
});
// Init with readable stream
Tinytest.addAsync('cfs-data - server - Init with readable stream', function(test, onComplete) {
streamData = new DataMan(fs.createReadStream(tempFilePath), "text/plain");
// filepaths are not converted to Buffers upon init
test.instanceOf(streamData.source, DataMan.ReadStream);
test.equal(streamData.type(), "text/plain");
onComplete();
});
// getBuffer
Tinytest.addAsync('cfs-data - server - getBuffer', function(test, onComplete) {
var total = 12, done = 0;
function testBuffer(error, buffer, testType) {
test.isFalse(!!error, testType + ' got error: ' + (error && error.message));
test.instanceOf(buffer, Buffer);
if (buffer instanceof Buffer) {
test.equal(buffer.toString(), 'Hello World', testType + ' got back buffer with incorrect data');
}
done++;
if (total === done) {
onComplete();
}
}
// from Buffer (async)
bufferData.getBuffer(function (error, buffer) {
testBuffer(error, buffer, 'getBuffer from Buffer async');
});
// from Buffer (sync)
testBuffer(null, bufferData.getBuffer(), 'getBuffer from Buffer sync');
// from ArrayBuffer (async)
arrayBufferData.getBuffer(function (error, buffer) {
testBuffer(error, buffer, 'getBuffer from ArrayBuffer async');
});
// from ArrayBuffer (sync)
testBuffer(null, arrayBufferData.getBuffer(), 'getBuffer from ArrayBuffer sync');
// from binary (async)
binaryData.getBuffer(function (error, buffer) {
testBuffer(error, buffer, 'getBuffer from binary async');
});
// from binary (sync)
testBuffer(null, binaryData.getBuffer(), 'getBuffer from binary sync');
// from data URI (async)
dataUriData.getBuffer(function (error, buffer) {
testBuffer(error, buffer, 'getBuffer from data URI async');
});
// from data URI (sync)
testBuffer(null, dataUriData.getBuffer(), 'getBuffer from data URI sync');
// from URL (async)
urlData.getBuffer(function (error, buffer) {
testBuffer(error, buffer, 'getBuffer from URL async');
});
// from URL (sync)
testBuffer(null, urlData.getBuffer(), 'getBuffer from URL sync');
// from filepath (async)
filePathData.getBuffer(function (error, buffer) {
testBuffer(error, buffer, 'getBuffer filepath async');
});
// from filepath (sync)
testBuffer(null, filePathData.getBuffer(), 'getBuffer filepath sync');
});
// getDataUri
Tinytest.addAsync('cfs-data - server - getDataUri', function(test, onComplete) {
var total = 12, done = 0;
function testURI(error, uri, testType) {
test.isFalse(!!error, testType + ' got error: ' + (error && error.message));
test.equal(typeof uri, "string", testType + ' got no URI string');
test.equal(uri, 'data:text/plain;base64,SGVsbG8gV29ybGQ=', testType + ' got invalid URI');
done++;
if (total === done) {
onComplete();
}
}
// from Buffer (async)
bufferData.getDataUri(function (error, uri) {
testURI(error, uri, 'getDataUri from Buffer async');
});
// from Buffer (sync)
testURI(null, bufferData.getDataUri(), 'getDataUri from Buffer sync');
// from ArrayBuffer (async)
arrayBufferData.getDataUri(function (error, uri) {
testURI(error, uri, 'getDataUri from ArrayBuffer async');
});
// from ArrayBuffer (sync)
testURI(null, arrayBufferData.getDataUri(), 'getDataUri from ArrayBuffer sync');
// from binary (async)
binaryData.getDataUri(function (error, uri) {
testURI(error, uri, 'getDataUri from binary async');
});
// from binary (sync)
testURI(null, binaryData.getDataUri(), 'getDataUri from binary sync');
// from data URI (async)
dataUriData.getDataUri(function (error, uri) {
testURI(error, uri, 'getDataUri from data URI async');
});
// from data URI (sync)
testURI(null, dataUriData.getDataUri(), 'getDataUri from data URI sync');
// from URL (async)
urlData.getDataUri(function (error, uri) {
testURI(error, uri, 'getDataUri from URL async');
});
// from URL (sync)
testURI(null, urlData.getDataUri(), 'getDataUri from URL sync');
// from filepath (async)
filePathData.getDataUri(function (error, uri) {
testURI(error, uri, 'getDataUri filepath async');
});
// from filepath (sync)
testURI(null, filePathData.getDataUri(), 'getDataUri filepath sync');
});
// size
Tinytest.addAsync('cfs-data - server - size', function(test, onComplete) {
var total = 6, done = 0;
function testSize(size, testType) {
test.equal(size, 11, testType + ' got wrong size');
done++;
if (total === done) {
onComplete();
}
}
// from Buffer
testSize(bufferData.size(), 'size from Buffer');
// from ArrayBuffer
testSize(arrayBufferData.size(), 'size from ArrayBuffer');
// from binary
testSize(binaryData.size(), 'size from binary');
// from data URI
testSize(dataUriData.size(), 'size from data URI');
// from URL
testSize(urlData.size(), 'size from URL');
// from filepath
testSize(filePathData.size(), 'size from filepath');
});
// saveToFile
// Since saveToFile uses createReadStream, this tests that function also
Tinytest.addAsync('cfs-data - server - saveToFile', function(test, onComplete) {
var total = 12, done = 0;
function testSave(dataInstance) {
var tempName = temp.path({suffix: '.txt'});
dataInstance.saveToFile(tempName, function (error) {
test.isFalse(!!error);
test.equal(fs.readFileSync(tempName, {encoding: 'utf8'}), 'Hello World', 'file was not saved with correct data');
done++;
if (total === done) {
onComplete();
}
});
}
function testSaveSync(dataInstance) {
var tempName = temp.path({suffix: '.txt'});
dataInstance.saveToFile(tempName);
test.equal(fs.readFileSync(tempName, {encoding: 'utf8'}), 'Hello World', 'file was not saved with correct data');
done++;
if (total === done) {
onComplete();
}
}
// from Buffer
testSave(bufferData);
testSaveSync(bufferData);
// from ArrayBuffer
testSave(arrayBufferData);
testSaveSync(arrayBufferData);
// from binary
testSave(binaryData);
testSaveSync(binaryData);
// from data URI
testSave(dataUriData);
testSaveSync(dataUriData);
// from URL
testSave(urlData);
testSaveSync(urlData);
// from filepath
testSave(filePathData);
testSaveSync(filePathData);
});
// Ensure that URL createReadStream can be piped after delay
// https://github.com/mikeal/request/issues/887
Tinytest.addAsync('cfs-data - server - createReadStream delay', function(test, onComplete) {
var readStream = urlData.createReadStream();
// wait for 5 seconds, then pipe
Meteor.setTimeout(function() {
var tempName = temp.path({suffix: '.txt'});
try {
var writeStream = readStream.pipe(fs.createWriteStream(tempName));
writeStream.on('finish', Meteor.bindEnvironment(function() {
test.equal(fs.readFileSync(tempName, {encoding: 'utf8'}), 'Hello World', 'file was not saved with correct data');
onComplete();
}));
writeStream.on('error', Meteor.bindEnvironment(function(err) {
test.isFalse(!!err);
}));
} catch (err) {
test.isFalse(!!err);
onComplete();
}
}, 5000);
});
//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,5 @@
language: node_js
node_js:
- "0.10"
before_install:
- "curl -L http://git.io/s0Zu-w | /bin/sh"

View file

@ -0,0 +1,283 @@
# Changelog
## vCurrent
## [v0.1.14] (https://github.com/zcfs/Meteor-cfs-file/tree/v0.1.14)
#### 17/12/14 by Morten Henriksen
- mbr update, remove versions.json
- Merge branch 'master' of https://github.com/zcfs/Meteor-cfs-file
- update pkg, dependencies, delinting, etc.
## [v0.1.13] (https://github.com/zcfs/Meteor-cfs-file/tree/v0.1.13)
#### 05/09/14 by Eric Dobbertin
- 0.9.1 support
## [v0.1.12] (https://github.com/zcfs/Meteor-cfs-file/tree/v0.1.12)
#### 28/08/14 by Morten Henriksen
- Meteor Package System Update
## [v0.1.11] (https://github.com/zcfs/Meteor-cfs-file/tree/v0.1.11)
#### 27/08/14 by Eric Dobbertin
- change package name to lowercase
## [v0.1.10] (https://github.com/zcfs/Meteor-cfs-file/tree/v0.1.10)
#### 31/07/14 by Eric Dobbertin
- allow FS.File to emit events
## [v0.1.9] (https://github.com/zcfs/Meteor-cfs-file/tree/v0.1.9)
#### 30/07/14 by Eric Dobbertin
- add fileObj.copy()
## [v0.1.8] (https://github.com/zcfs/Meteor-cfs-file/tree/v0.1.8)
#### 14/07/14 by Eric Dobbertin
- make setters save to DB automatically with ability to disable
## [v0.1.7] (https://github.com/zcfs/Meteor-cfs-file/tree/v0.1.7)
#### 30/06/14 by Eric Dobbertin
- *Fixed bug:* "fsFile-common.js:66:8: Unexpected identifier" [#3](https://github.com/zcfs/Meteor-cfs-file/issues/3)
## [v0.1.6] (https://github.com/zcfs/Meteor-cfs-file/tree/v0.1.6)
#### 30/06/14 by Eric Dobbertin
- a few more changes for supporting URL options
- support request options like `auth` and `headers` when attaching a URL
## [v0.1.5] (https://github.com/zcfs/Meteor-cfs-file/tree/v0.1.5)
#### 20/05/14 by Eric Dobbertin
- add almost all necessary tests!
- fix updatedAt setting
- correct logic
- store size as number
- API doc fixes
## [v0.1.4] (https://github.com/zcfs/Meteor-cfs-file/tree/v0.1.4)
#### 11/05/14 by Eric Dobbertin
- document API for createReadStream and createWriteStream
## [v0.1.3] (https://github.com/zcfs/Meteor-cfs-file/tree/v0.1.3)
#### 06/05/14 by Eric Dobbertin
- missed this during api change
## [v0.1.2] (https://github.com/zcfs/Meteor-cfs-file/tree/v0.1.2)
#### 30/04/14 by Eric Dobbertin
- updating from DB before getting should be opt-in instead of opt-out; it's more efficient this way, and it prevents issues with local props unintentionally being overwritten
## [v0.1.1] (https://github.com/zcfs/Meteor-cfs-file/tree/v0.1.1)
#### 29/04/14 by Eric Dobbertin
- add `extension` getter/setter method and make the other new methods work as UI helpers
## [v0.1.0] (https://github.com/zcfs/Meteor-cfs-file/tree/v0.1.0)
#### 29/04/14 by Eric Dobbertin
- generate api docs
- add updatedAt method
- provide option to skip updating from the file record before getting info
- more changes for new name, size, and type API
- add getter/setter methods for name, size, and type; move original file info under `original` property object backwards compatibility break!
- change name of `hasCopy` function to `hasStored`
## [v0.0.34] (https://github.com/zcfs/Meteor-cfs-file/tree/v0.0.34)
#### 18/04/14 by Eric Dobbertin
- add formattedSize method and weak dependency on numeral pkg
## [v0.0.33] (https://github.com/zcfs/Meteor-cfs-file/tree/v0.0.33)
#### 15/04/14 by Eric Dobbertin
- call attachData only if we have some data
## [v0.0.32] (https://github.com/zcfs/Meteor-cfs-file/tree/v0.0.32)
#### 08/04/14 by Eric Dobbertin
- throw a more helpful error if null/undefined data is passed to attachData
- remove `get` method; the code in it is old and would fail so I'm assuming nothing calls it anymore
## [v0.0.31] (https://github.com/zcfs/Meteor-cfs-file/tree/v0.0.31)
#### 07/04/14 by Eric Dobbertin
- allow passing any data (except URL on client) to FS.File constructor to attach it
## [v0.0.30] (https://github.com/zcfs/Meteor-cfs-file/tree/v0.0.30)
#### 07/04/14 by Eric Dobbertin
- make it safe to call attachData without a callback on the client, unless we are attaching a URL; in that case, an error is thrown if no callback
## [v0.0.29] (https://github.com/zcfs/Meteor-cfs-file/tree/v0.0.29)
#### 06/04/14 by Eric Dobbertin
- use full clone to fix issue with saving FS.File into a collection using EJSON
## [v0.0.28] (https://github.com/zcfs/Meteor-cfs-file/tree/v0.0.28)
#### 06/04/14 by Eric Dobbertin
- use uploadedAt so that we can remove chunk info when it's no longer needed
## [v0.0.27] (https://github.com/zcfs/Meteor-cfs-file/tree/v0.0.27)
#### 06/04/14 by Eric Dobbertin
- improve setName a bit and move some logic to FS.Utility
## [v0.0.26] (https://github.com/zcfs/Meteor-cfs-file/tree/v0.0.26)
#### 05/04/14 by Eric Dobbertin
- set name when inserting filepath without callback
## [v0.0.25] (https://github.com/zcfs/Meteor-cfs-file/tree/v0.0.25)
#### 03/04/14 by Eric Dobbertin
- Make sure passing in a storeName works
## [v0.0.24] (https://github.com/zcfs/Meteor-cfs-file/tree/v0.0.24)
#### 03/04/14 by Eric Dobbertin
- move all data handling to data-man package; fix issue getting size properly
## [v0.0.23] (https://github.com/zcfs/Meteor-cfs-file/tree/v0.0.23)
#### 02/04/14 by Morten Henriksen
- We should allow size to be updated if org size is 0
## [v0.0.22] (https://github.com/zcfs/Meteor-cfs-file/tree/v0.0.22)
#### 31/03/14 by Eric Dobbertin
- use latest releases
## [v0.0.21] (https://github.com/zcfs/Meteor-cfs-file/tree/v0.0.21)
#### 29/03/14 by Morten Henriksen
- remove underscore deps
## [v0.0.20] (https://github.com/zcfs/Meteor-cfs-file/tree/v0.0.20)
#### 25/03/14 by Morten Henriksen
- refactor utime into updatedAt
## [v0.0.19] (https://github.com/zcfs/Meteor-cfs-file/tree/v0.0.19)
#### 23/03/14 by Morten Henriksen
- Rollback to specific git dependency
- use collectionFS travis version force update
## [v0.0.18] (https://github.com/zcfs/Meteor-cfs-file/tree/v0.0.18)
#### 22/03/14 by Morten Henriksen
- try to fix travis test by using general package references
## [v0.0.17] (https://github.com/zcfs/Meteor-cfs-file/tree/v0.0.17)
#### 21/03/14 by Morten Henriksen
- remove smart.lock
- adjustments, trying to make everything work in phantomjs
- add server tests for FS.Data and fix found issues; add Blob.js polyfill so that we have phantomjs support (and travis tests can pass)
- ignore potential query string when seeing if URL ends in filename
## [v0.0.16] (https://github.com/zcfs/Meteor-cfs-file/tree/v0.0.16)
#### 20/03/14 by Eric Dobbertin
- set size when attaching filepath
- add travis-ci image
- package changes to fix auto test issues
- should not have been committed
- we should ignore packages folder entirely
- Return self from attachData, and adjust code in other areas to keep the flow of all methods similar
## [v0.0.15] (https://github.com/zcfs/Meteor-cfs-file/tree/v0.0.15)
#### 18/03/14 by Eric Dobbertin
- See if we can extract a file name from URL or filepath
- use utility function to get file extension
## [v0.0.14] (https://github.com/zcfs/Meteor-cfs-file/tree/v0.0.14)
#### 18/03/14 by Eric Dobbertin
- remove code that deleted files from temp store; the worker does this in a remove observe
## [v0.0.13] (https://github.com/zcfs/Meteor-cfs-file/tree/v0.0.13)
#### 18/03/14 by Morten Henriksen
- Add note about dataUrl references
- fixed issue #204
- client-side tests for FS.Data (!) and fix failed tests
- allow attachData to run synchronous on server even when attaching URL
- fix some data attachment issues and move file allowed check to FS.Collection
- silently handle non-object args
- refactor FS.File.createWriteStream to support TempStore and FileWorker
- tidy the FS.File.createReadStream
- update API docs
- use correct api
- Merge branch 'master' of https://github.com/zcfs/Meteor-cfs-file
- Complete rewrite; better split between client/server, move data handling to FS.Data class, and support streams
- use chunks in the filerecord
- make sure we have a downloadqueue
## [v0.0.12] (https://github.com/zcfs/Meteor-cfs-file/tree/v0.0.12)
#### 07/03/14 by Eric Dobbertin
- should not require a filename; check extension only if we have one
- allow `FS.File.fromUrl` to run synchronously without a callback on the server
## [v0.0.11] (https://github.com/zcfs/Meteor-cfs-file/tree/v0.0.11)
#### 03/03/14 by Eric Dobbertin
- add `format` option for FS.File.prototype.get
- Merge branch 'master' of https://github.com/zcfs/Meteor-cfs-file
## [v0.0.10] (https://github.com/zcfs/Meteor-cfs-file/tree/v0.0.10)
#### 01/03/14 by Eric Dobbertin
- add pkg necessary for tests
## [v0.0.9] (https://github.com/zcfs/Meteor-cfs-file/tree/v0.0.9)
#### 28/02/14 by Eric Dobbertin
- changes for http uploads
## [v0.0.8] (https://github.com/zcfs/Meteor-cfs-file/tree/v0.0.8)
#### 28/02/14 by Eric Dobbertin
- fix issues with callbacks
## [v0.0.7] (https://github.com/zcfs/Meteor-cfs-file/tree/v0.0.7)
#### 21/02/14 by Eric Dobbertin
- new URL syntax; use the store's file key instead of ID
## [v0.0.6] (https://github.com/zcfs/Meteor-cfs-file/tree/v0.0.6)
#### 17/02/14 by Morten Henriksen
- add getCopyInfo method
## [v0.0.5] (https://github.com/zcfs/Meteor-cfs-file/tree/v0.0.5)
#### 16/02/14 by Morten Henriksen
- added public
## [v0.0.4] (https://github.com/zcfs/Meteor-cfs-file/tree/v0.0.4)
#### 16/02/14 by Morten Henriksen
- fix url
- use generic http url
## [v0.0.3] (https://github.com/zcfs/Meteor-cfs-file/tree/v0.0.3)
#### 15/02/14 by Morten Henriksen
- reference cfs-filesaver pkg instead
- rework http/ddp method init; also DDP methods don't need to be per-collection so they no longer are
- minor adjustments; add missing FileSaver.js
## [v0.0.2] (https://github.com/zcfs/Meteor-cfs-file/tree/v0.0.2)
#### 13/02/14 by Morten Henriksen
- Fixed getter of storage adapters
## [v0.0.1] (https://github.com/zcfs/Meteor-cfs-file/tree/v0.0.1)
#### 13/02/14 by Morten Henriksen
- Corrections to new scope
- init commit

View file

@ -0,0 +1,20 @@
The MIT License (MIT)
Copyright (c) 2013-2014 [@raix](https://github.com/raix), aka Morten N.O. Nørgaard Henriksen, mh@gi-software.com, and [@aldeed](https://github.com/aldeed)
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,8 @@
wekan-cfs-file [![Build Status](https://travis-ci.org/CollectionFS/Meteor-cfs-file.png?branch=master)](https://travis-ci.org/CollectionFS/Meteor-cfs-file)
=========================
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.

View file

@ -0,0 +1,588 @@
## wekan-cfs-file Public API ##
CollectionFS, FS.File object
_API documentation automatically generated by [docmeteor](https://github.com/raix/docmeteor)._
-
### <a name="FS.File"></a>new *fs*.File([ref])&nbsp;&nbsp;<sub><i>Anywhere</i></sub> ###
*This method __File__ is defined in `FS`*
__Arguments__
* __ref__ *{object|[FS.File](#FS.File)|[data to attach](#data to attach)}* (Optional)
Another FS.File instance, a filerecord, or some data to pass to attachData
> ```FS.File = function(ref, createdByTransform) { ...``` [fsFile-common.js:8](fsFile-common.js#L8)
-
### <a name="FS.File.prototype.attachData"></a>*fsFile*.attachData(data, [options], [callback])&nbsp;&nbsp;<sub><i>Anywhere</i></sub> ###
*This method __attachData__ is defined in `prototype` of `FS.File`*
__Arguments__
* __data__ *{[File](#File)|[Blob](#Blob)|Buffer|ArrayBuffer|Uint8Array|String}*
The data that you want to attach to the file.
* __options__ *{Object}* (Optional)
Options
* __type__ *{String}* (Optional)
The data content (MIME) type, if known.
* __headers__ *{String}* (Optional)
When attaching a URL, headers to be used for the GET request (currently server only)
* __auth__ *{String}* (Optional)
When attaching a URL, "username:password" to be used for the GET request (currently server only)
* __callback__ *{Function}* (Optional)
Callback function, callback(error). On the client, a callback is required if data is a URL.
__Returns__ *{FS.File}*
This FS.File instance.
> ```FS.File.prototype.attachData = function fsFileAttachData(data, options, callback) { ...``` [fsFile-common.js:36](fsFile-common.js#L36)
-
### <a name="FS.File.prototype.uploadProgress"></a>*fsFile*.uploadProgress()&nbsp;&nbsp;<sub><i>Anywhere</i></sub> ###
*This method __uploadProgress__ is defined in `prototype` of `FS.File`*
__Returns__ *{number}*
The server confirmed upload progress
> ```FS.File.prototype.uploadProgress = function() { ...``` [fsFile-common.js:154](fsFile-common.js#L154)
-
### <a name="FS.File.prototype.controlledByDeps"></a>*fsFile*.controlledByDeps()&nbsp;&nbsp;<sub><i>Anywhere</i></sub> ###
*This method __controlledByDeps__ is defined in `prototype` of `FS.File`*
__Returns__ *{FS.Collection}*
Returns true if this FS.File is reactive
> Note: Returns true if this FS.File object was created by a FS.Collection
> and we are in a reactive computations. What does this mean? Well it should
> mean that our fileRecord is fully updated by Meteor and we are mounted on
> a collection
> ```FS.File.prototype.controlledByDeps = function() { ...``` [fsFile-common.js:179](fsFile-common.js#L179)
-
### <a name="FS.File.prototype.getCollection"></a>*fsFile*.getCollection()&nbsp;&nbsp;<sub><i>Anywhere</i></sub> ###
*This method __getCollection__ is defined in `prototype` of `FS.File`*
__Returns__ *{FS.Collection}*
Returns attached collection or undefined if not mounted
> ```FS.File.prototype.getCollection = function() { ...``` [fsFile-common.js:189](fsFile-common.js#L189)
-
### <a name="FS.File.prototype.isMounted"></a>*fsFile*.isMounted()&nbsp;&nbsp;<sub><i>Anywhere</i></sub> ###
*This method __isMounted__ is defined in `prototype` of `FS.File`*
__Returns__ *{FS.Collection}*
Returns attached collection or undefined if not mounted
> ```FS.File.prototype.isMounted = FS.File.prototype.getCollection;``` [fsFile-common.js:217](fsFile-common.js#L217)
-
### <a name="FS.File.prototype.getFileRecord"></a>*fsFile*.getFileRecord()&nbsp;&nbsp;<sub><i>Anywhere</i></sub> ###
*This method __getFileRecord__ is defined in `prototype` of `FS.File`*
__Returns__ *{object}*
The filerecord
> ```FS.File.prototype.getFileRecord = function() { ...``` [fsFile-common.js:224](fsFile-common.js#L224)
-
### <a name="FS.File.prototype.update"></a>*fsFile*.update(modifier, [options], [callback])&nbsp;&nbsp;<sub><i>Anywhere</i></sub> ###
*This method __update__ is defined in `prototype` of `FS.File`*
__Arguments__
* __modifier__ *{[modifier](#modifier)}*
* __options__ *{object}* (Optional)
* __callback__ *{function}* (Optional)
Updates the fileRecord.
> ```FS.File.prototype.update = function(modifier, options, callback) { ...``` [fsFile-common.js:255](fsFile-common.js#L255)
-
### <a name="FS.File.prototype.remove"></a>*fsFile*.remove([callback])&nbsp;&nbsp;<sub><i>Anywhere</i></sub> ###
*This method __remove__ is defined in `prototype` of `FS.File`*
__Arguments__
* __callback__ *{Function}* (Optional)
__Returns__ *{number}*
Count
Remove the current file from its FS.Collection
> ```FS.File.prototype.remove = function(callback) { ...``` [fsFile-common.js:323](fsFile-common.js#L323)
-
### <a name="FS.File.prototype.getExtension"></a>*fsFile*.getExtension([options])&nbsp;&nbsp;<sub><i>Anywhere</i></sub> ###
> __Warning!__
> This method "FS.File.prototype.getExtension" has deprecated from the API
> Use the `extension` getter/setter method instead.
*This method __getExtension__ is defined in `prototype` of `FS.File`*
__Arguments__
* __options__ *{Object}* (Optional)
* __store__ *{String}* (Optional)
Store name. Default is the original extension.
__Returns__ *{string}*
The extension eg.: `jpg` or if not found then an empty string ''
> ```FS.File.prototype.getExtension = function(options) { ...``` [fsFile-common.js:364](fsFile-common.js#L364)
-
### <a name="FS.File.prototype.isImage"></a>*fsFile*.isImage([options])&nbsp;&nbsp;<sub><i>Anywhere</i></sub> ###
*This method __isImage__ is defined in `prototype` of `FS.File`*
__Arguments__
* __options__ *{object}* (Optional)
* __store__ *{string}* (Optional)
The store we're interested in
Returns true if the copy of this file in the specified store has an image
content type. If the file object is unmounted or doesn't have a copy for
the specified store, or if you don't specify a store, this method checks
the content type of the original file.
> ```FS.File.prototype.isImage = function(options) { ...``` [fsFile-common.js:393](fsFile-common.js#L393)
-
### <a name="FS.File.prototype.isVideo"></a>*fsFile*.isVideo([options])&nbsp;&nbsp;<sub><i>Anywhere</i></sub> ###
*This method __isVideo__ is defined in `prototype` of `FS.File`*
__Arguments__
* __options__ *{object}* (Optional)
* __store__ *{string}* (Optional)
The store we're interested in
Returns true if the copy of this file in the specified store has a video
content type. If the file object is unmounted or doesn't have a copy for
the specified store, or if you don't specify a store, this method checks
the content type of the original file.
> ```FS.File.prototype.isVideo = function(options) { ...``` [fsFile-common.js:408](fsFile-common.js#L408)
-
### <a name="FS.File.prototype.isAudio"></a>*fsFile*.isAudio([options])&nbsp;&nbsp;<sub><i>Anywhere</i></sub> ###
*This method __isAudio__ is defined in `prototype` of `FS.File`*
__Arguments__
* __options__ *{object}* (Optional)
* __store__ *{string}* (Optional)
The store we're interested in
Returns true if the copy of this file in the specified store has an audio
content type. If the file object is unmounted or doesn't have a copy for
the specified store, or if you don't specify a store, this method checks
the content type of the original file.
> ```FS.File.prototype.isAudio = function(options) { ...``` [fsFile-common.js:423](fsFile-common.js#L423)
-
### <a name="FS.File.prototype.formattedSize"></a>*fsFile*.formattedSize({Object}, {String}, {String})&nbsp;&nbsp;<sub><i>Anywhere</i></sub> ###
*This method __formattedSize__ is defined in `prototype` of `FS.File`*
__Arguments__
* __{Object}__ *{any}*
options
* __{String}__ *{any}*
[options.store=none,display original file size] Which file do you want to get the size of?
* __{String}__ *{any}*
[options.formatString='0.00 b'] The `numeral` format string to use.
__Returns__ *{String}*
The file size formatted as a human readable string and reactively updated.
You must add the `numeral` package to your app before you can use this method.
If info is not found or a size can't be determined, it will show 0.
> ```FS.File.prototype.formattedSize = function fsFileFormattedSize(options) { ...``` [fsFile-common.js:438](fsFile-common.js#L438)
-
### <a name="FS.File.prototype.isUploaded"></a>*fsFile*.isUploaded()&nbsp;&nbsp;<sub><i>Anywhere</i></sub> ###
*This method __isUploaded__ is defined in `prototype` of `FS.File`*
__Returns__ *{boolean}*
True if the number of uploaded bytes is equal to the file size.
> ```FS.File.prototype.isUploaded = function() { ...``` [fsFile-common.js:456](fsFile-common.js#L456)
-
### <a name="FS.File.prototype.hasStored"></a>*fsFile*.hasStored(storeName, [optimistic])&nbsp;&nbsp;<sub><i>Anywhere</i></sub> ###
*This method __hasStored__ is defined in `prototype` of `FS.File`*
__Arguments__
* __storeName__ *{string}*
Name of the store
* __optimistic__ *{boolean}* (Optional, Default = false)
In case that the file record is not found, read below
__Returns__ *{boolean}*
Is a version of this file stored in the given store?
> Note: If the file is not published to the client or simply not found:
this method cannot know for sure if it exists or not. The `optimistic`
param is the boolean value to return. Are we `optimistic` that the copy
could exist. This is the case in `FS.File.url` we are optimistic that the
copy supplied by the user exists.
> ```FS.File.prototype.hasStored = function(storeName, optimistic) { ...``` [fsFile-common.js:478](fsFile-common.js#L478)
-
### <a name="FS.File.prototype.getCopyInfo"></a>*fsFile*.getCopyInfo(storeName)&nbsp;&nbsp;<sub><i>Anywhere</i></sub> ###
> __Warning!__
> This method "FS.File.prototype.getCopyInfo" has deprecated from the API
> Use individual methods with `store` option instead.
*This method __getCopyInfo__ is defined in `prototype` of `FS.File`*
__Arguments__
* __storeName__ *{string}*
Name of the store for which to get copy info.
__Returns__ *{Object}*
The file details, e.g., name, size, key, etc., specific to the copy saved in this store.
> ```FS.File.prototype.getCopyInfo = function(storeName) { ...``` [fsFile-common.js:504](fsFile-common.js#L504)
-
### <a name="FS.File.prototype.name"></a>*fsFile*.name([value], [options])&nbsp;&nbsp;<sub><i>Anywhere</i></sub> ###
*This method __name__ is defined in `prototype` of `FS.File`*
__Arguments__
* __value__ *{String|null}* (Optional)
If setting the name, specify the new name as the first argument. Otherwise the options argument should be first.
* __options__ *{Object}* (Optional)
* __store__ *{Object}* (Optional, Default = none,original)
Get or set the name of the version of the file that was saved in this store. Default is the original file name.
* __updateFileRecordFirst__ *{Boolean}* (Optional, Default = false)
Update this instance with data from the DB first? Applies to getter usage only.
* __save__ *{Boolean}* (Optional, Default = true)
Save change to database? Applies to setter usage only.
__Returns__ *{String|undefined}*
If setting, returns `undefined`. If getting, returns the file name.
> ```FS.File.prototype.name = function(value, options) { ...``` [fsFile-common.js:568](fsFile-common.js#L568)
-
### <a name="FS.File.prototype.extension"></a>*fsFile*.extension([value], [options])&nbsp;&nbsp;<sub><i>Anywhere</i></sub> ###
*This method __extension__ is defined in `prototype` of `FS.File`*
__Arguments__
* __value__ *{String|null}* (Optional)
If setting the extension, specify the new extension (without period) as the first argument. Otherwise the options argument should be first.
* __options__ *{Object}* (Optional)
* __store__ *{Object}* (Optional, Default = none,original)
Get or set the extension of the version of the file that was saved in this store. Default is the original file extension.
* __updateFileRecordFirst__ *{Boolean}* (Optional, Default = false)
Update this instance with data from the DB first? Applies to getter usage only.
* __save__ *{Boolean}* (Optional, Default = true)
Save change to database? Applies to setter usage only.
__Returns__ *{String|undefined}*
If setting, returns `undefined`. If getting, returns the file extension or an empty string if there isn't one.
> ```FS.File.prototype.extension = function(value, options) { ...``` [fsFile-common.js:593](fsFile-common.js#L593)
-
### <a name="FS.File.prototype.size"></a>*fsFile*.size([value], [options])&nbsp;&nbsp;<sub><i>Anywhere</i></sub> ###
*This method __size__ is defined in `prototype` of `FS.File`*
__Arguments__
* __value__ *{Number}* (Optional)
If setting the size, specify the new size in bytes as the first argument. Otherwise the options argument should be first.
* __options__ *{Object}* (Optional)
* __store__ *{Object}* (Optional, Default = none,original)
Get or set the size of the version of the file that was saved in this store. Default is the original file size.
* __updateFileRecordFirst__ *{Boolean}* (Optional, Default = false)
Update this instance with data from the DB first? Applies to getter usage only.
* __save__ *{Boolean}* (Optional, Default = true)
Save change to database? Applies to setter usage only.
__Returns__ *{Number|undefined}*
If setting, returns `undefined`. If getting, returns the file size.
> ```FS.File.prototype.size = function(value, options) { ...``` [fsFile-common.js:618](fsFile-common.js#L618)
-
### <a name="FS.File.prototype.type"></a>*fsFile*.type([value], [options])&nbsp;&nbsp;<sub><i>Anywhere</i></sub> ###
*This method __type__ is defined in `prototype` of `FS.File`*
__Arguments__
* __value__ *{String}* (Optional)
If setting the type, specify the new type as the first argument. Otherwise the options argument should be first.
* __options__ *{Object}* (Optional)
* __store__ *{Object}* (Optional, Default = none,original)
Get or set the type of the version of the file that was saved in this store. Default is the original file type.
* __updateFileRecordFirst__ *{Boolean}* (Optional, Default = false)
Update this instance with data from the DB first? Applies to getter usage only.
* __save__ *{Boolean}* (Optional, Default = true)
Save change to database? Applies to setter usage only.
__Returns__ *{String|undefined}*
If setting, returns `undefined`. If getting, returns the file type.
> ```FS.File.prototype.type = function(value, options) { ...``` [fsFile-common.js:643](fsFile-common.js#L643)
-
### <a name="FS.File.prototype.updatedAt"></a>*fsFile*.updatedAt([value], [options])&nbsp;&nbsp;<sub><i>Anywhere</i></sub> ###
*This method __updatedAt__ is defined in `prototype` of `FS.File`*
__Arguments__
* __value__ *{String}* (Optional)
If setting updatedAt, specify the new date as the first argument. Otherwise the options argument should be first.
* __options__ *{Object}* (Optional)
* __store__ *{Object}* (Optional, Default = none,original)
Get or set the last updated date for the version of the file that was saved in this store. Default is the original last updated date.
* __updateFileRecordFirst__ *{Boolean}* (Optional, Default = false)
Update this instance with data from the DB first? Applies to getter usage only.
* __save__ *{Boolean}* (Optional, Default = true)
Save change to database? Applies to setter usage only.
__Returns__ *{String|undefined}*
If setting, returns `undefined`. If getting, returns the file's last updated date.
> ```FS.File.prototype.updatedAt = function(value, options) { ...``` [fsFile-common.js:668](fsFile-common.js#L668)
-
### <a name="FS.File.prototype.createReadStream"></a>*fsFile*.createReadStream([storeName])&nbsp;&nbsp;<sub><i>Server</i></sub> ###
*This method __createReadStream__ is defined in `prototype` of `FS.File`*
__Arguments__
* __storeName__ *{String}* (Optional)
__Returns__ *{stream.Readable}*
Readable NodeJS stream
Returns a readable stream. Where the stream reads from depends on the FS.File instance and whether you pass a store name.
If you pass a `storeName`, a readable stream for the file data saved in that store is returned.
If you don't pass a `storeName` and data is attached to the FS.File instance (on `data` property, which must be a DataMan instance), then a readable stream for the attached data is returned.
If you don't pass a `storeName` and there is no data attached to the FS.File instance, a readable stream for the file data currently in the temporary store (`FS.TempStore`) is returned.
> ```FS.File.prototype.createReadStream = function(storeName) { ...``` [fsFile-server.js:62](fsFile-server.js#L62)
-
### <a name="FS.File.prototype.createWriteStream"></a>*fsFile*.createWriteStream([storeName])&nbsp;&nbsp;<sub><i>Server</i></sub> ###
*This method __createWriteStream__ is defined in `prototype` of `FS.File`*
__Arguments__
* __storeName__ *{String}* (Optional)
__Returns__ *{stream.Writeable}*
Writeable NodeJS stream
Returns a writeable stream. Where the stream writes to depends on whether you pass in a store name.
If you pass a `storeName`, a writeable stream for (over)writing the file data in that store is returned.
If you don't pass a `storeName`, a writeable stream for writing to the temp store for this file is returned.
> ```FS.File.prototype.createWriteStream = function(storeName) { ...``` [fsFile-server.js:100](fsFile-server.js#L100)
-
### <a name="FS.File.prototype.copy"></a>*fsFile*.copy()&nbsp;&nbsp;<sub><i>Server</i></sub> ###
*This method __copy__ is defined in `prototype` of `FS.File`*
__Returns__ *{FS.File}*
The new FS.File instance
> ```FS.File.prototype.copy = function() { ...``` [fsFile-server.js:126](fsFile-server.js#L126)

View file

@ -0,0 +1,765 @@
/**
* @method FS.File
* @namespace FS.File
* @public
* @constructor
* @param {object|FS.File|data to attach} [ref] Another FS.File instance, a filerecord, or some data to pass to attachData
*/
FS.File = function(ref, createdByTransform) {
var self = this;
self.createdByTransform = !!createdByTransform;
if (ref instanceof FS.File || isBasicObject(ref)) {
// Extend self with filerecord related data
FS.Utility.extend(self, FS.Utility.cloneFileRecord(ref, {full: true}));
} else if (ref) {
self.attachData(ref);
}
};
// An FS.File can emit events
FS.File.prototype = new EventEmitter();
/**
* @method FS.File.prototype.attachData
* @public
* @param {File|Blob|Buffer|ArrayBuffer|Uint8Array|String} data The data that you want to attach to the file.
* @param {Object} [options] Options
* @param {String} [options.type] The data content (MIME) type, if known.
* @param {String} [options.headers] When attaching a URL, headers to be used for the GET request (currently server only)
* @param {String} [options.auth] When attaching a URL, "username:password" to be used for the GET request (currently server only)
* @param {Function} [callback] Callback function, callback(error). On the client, a callback is required if data is a URL.
* @returns {FS.File} This FS.File instance.
*
*/
FS.File.prototype.attachData = function fsFileAttachData(data, options, callback) {
var self = this;
if (!callback && typeof options === "function") {
callback = options;
options = {};
}
options = options || {};
if (!data) {
throw new Error('FS.File.attachData requires a data argument with some data');
}
var urlOpts;
// Set any other properties we can determine from the source data
// File
if (typeof File !== "undefined" && data instanceof File) {
self.name(data.name);
self.updatedAt(data.lastModifiedDate);
self.size(data.size);
setData(data.type);
}
// Blob
else if (typeof Blob !== "undefined" && data instanceof Blob) {
self.name(data.name);
self.updatedAt(new Date());
self.size(data.size);
setData(data.type);
}
// URL: we need to do a HEAD request to get the type because type
// is required for filtering to work.
else if (typeof data === "string" && (data.slice(0, 5) === "http:" || data.slice(0, 6) === "https:")) {
urlOpts = FS.Utility.extend({}, options);
if (urlOpts.type) {
delete urlOpts.type;
}
if (!callback) {
if (Meteor.isClient) {
throw new Error('FS.File.attachData requires a callback when attaching a URL on the client');
}
var result = Meteor.call('_cfs_getUrlInfo', data, urlOpts);
FS.Utility.extend(self, {original: result});
setData(result.type);
} else {
Meteor.call('_cfs_getUrlInfo', data, urlOpts, function (error, result) {
FS.debug && console.log("URL HEAD RESULT:", result);
if (error) {
callback(error);
} else {
var type = result.type || options.type;
if (! type) {
throw new Error('FS.File.attachData got a URL for which it could not determine the MIME type and none was provided using options.type');
}
FS.Utility.extend(self, {original: result});
setData(type);
}
});
}
}
// Everything else
else {
setData(options.type);
}
// Set the data
function setData(type) {
self.data = new DataMan(data, type, urlOpts);
// Update the type to match what the data is
self.type(self.data.type());
// Update the size to match what the data is.
// It's always safe to call self.data.size() without supplying a callback
// because it requires a callback only for URLs on the client, and we
// already added size for URLs when we got the result from '_cfs_getUrlInfo' method.
if (!self.size()) {
if (callback) {
self.data.size(function (error, size) {
if (error) {
callback && callback(error);
} else {
self.size(size);
setName();
}
});
} else {
self.size(self.data.size());
setName();
}
} else {
setName();
}
}
function setName() {
// See if we can extract a file name from URL or filepath
if (!self.name() && typeof data === "string") {
// name from URL
if (data.slice(0, 5) === "http:" || data.slice(0, 6) === "https:") {
if (FS.Utility.getFileExtension(data).length) {
// for a URL we assume the end is a filename only if it has an extension
self.name(FS.Utility.getFileName(data));
}
}
// name from filepath
else if (data.slice(0, 5) !== "data:") {
self.name(FS.Utility.getFileName(data));
}
}
callback && callback();
}
return self; //allow chaining
};
/**
* @method FS.File.prototype.uploadProgress
* @public
* @returns {number} The server confirmed upload progress
*/
FS.File.prototype.uploadProgress = function() {
var self = this;
// Make sure our file record is updated
self.getFileRecord();
// If fully uploaded, return 100
if (self.uploadedAt) {
return 100;
}
// Otherwise return the confirmed progress or 0
else {
return Math.round((self.chunkCount || 0) / (self.chunkSum || 1) * 100);
}
};
/**
* @method FS.File.prototype.controlledByDeps
* @public
* @returns {FS.Collection} Returns true if this FS.File is reactive
*
* > Note: Returns true if this FS.File object was created by a FS.Collection
* > and we are in a reactive computations. What does this mean? Well it should
* > mean that our fileRecord is fully updated by Meteor and we are mounted on
* > a collection
*/
FS.File.prototype.controlledByDeps = function() {
var self = this;
return self.createdByTransform && Deps.active;
};
/**
* @method FS.File.prototype.getCollection
* @public
* @returns {FS.Collection} Returns attached collection or undefined if not mounted
*/
FS.File.prototype.getCollection = function() {
// Get the collection reference
var self = this;
// If we already made the link then do no more
if (self.collection) {
return self.collection;
}
// If we don't have a collectionName then there's not much to do, the file is
// not mounted yet
if (!self.collectionName) {
// Should not throw an error here - could be common that the file is not
// yet mounted into a collection
return;
}
// Link the collection to the file
self.collection = FS._collections[self.collectionName];
return self.collection; //possibly undefined, but that's desired behavior
};
/**
* @method FS.File.prototype.isMounted
* @public
* @returns {FS.Collection} Returns attached collection or undefined if not mounted
*/
FS.File.prototype.isMounted = FS.File.prototype.getCollection;
/**
* @method FS.File.prototype.getFileRecord Returns the fileRecord
* @public
* @returns {object} The filerecord
*/
FS.File.prototype.getFileRecord = function() {
var self = this;
// Check if this file object fileRecord is kept updated by Meteor, if so
// return self
if (self.controlledByDeps()) {
return self;
}
// Go for manually updating the file record
if (self.isMounted()) {
FS.debug && console.log('GET FILERECORD: ' + self._id);
// Return the fileRecord or an empty object
var fileRecord = self.collection.files.findOne({_id: self._id}) || {};
FS.Utility.extend(self, fileRecord);
return fileRecord;
} else {
// We return an empty object, this way users can still do `getRecord().size`
// Without getting an error
return {};
}
};
/**
* @method FS.File.prototype.update
* @public
* @param {modifier} modifier
* @param {object} [options]
* @param {function} [callback]
*
* Updates the fileRecord.
*/
FS.File.prototype.update = function(modifier, options, callback) {
var self = this;
FS.debug && console.log('UPDATE: ' + JSON.stringify(modifier));
// Make sure we have options and callback
if (!callback && typeof options === 'function') {
callback = options;
options = {};
}
callback = callback || FS.Utility.defaultCallback;
if (!self.isMounted()) {
callback(new Error("Cannot update a file that is not associated with a collection"));
return;
}
// Call collection update - File record
return self.collection.files.update({_id: self._id}, modifier, options, function(err, count) {
// Update the fileRecord if it was changed and on the client
// The server-side methods will pull the fileRecord if needed
if (count > 0 && Meteor.isClient)
self.getFileRecord();
// Call callback
callback(err, count);
});
};
/**
* @method FS.File.prototype._saveChanges
* @private
* @param {String} [what] "_original" to save original info, or a store name to save info for that store, or saves everything
*
* Updates the fileRecord from values currently set on the FS.File instance.
*/
FS.File.prototype._saveChanges = function(what) {
var self = this;
if (!self.isMounted()) {
return;
}
FS.debug && console.log("FS.File._saveChanges:", what || "all");
var mod = {$set: {}};
if (what === "_original") {
mod.$set.original = self.original;
} else if (typeof what === "string") {
var info = self.copies[what];
if (info) {
mod.$set["copies." + what] = info;
}
} else {
mod.$set.original = self.original;
mod.$set.copies = self.copies;
}
self.update(mod);
};
/**
* @method FS.File.prototype.remove
* @public
* @param {Function} [callback]
* @returns {number} Count
*
* Remove the current file from its FS.Collection
*/
FS.File.prototype.remove = function(callback) {
var self = this;
FS.debug && console.log('REMOVE: ' + self._id);
callback = callback || FS.Utility.defaultCallback;
if (!self.isMounted()) {
callback(new Error("Cannot remove a file that is not associated with a collection"));
return;
}
return self.collection.files.remove({_id: self._id}, function(err, res) {
if (!err) {
delete self._id;
delete self.collection;
delete self.collectionName;
}
callback(err, res);
});
};
/**
* @method FS.File.prototype.moveTo
* @param {FS.Collection} targetCollection
* @private // Marked private until implemented
* @todo Needs to be implemented
*
* Move the file from current collection to another collection
*
* > Note: Not yet implemented
*/
/**
* @method FS.File.prototype.getExtension Returns the lowercase file extension
* @public
* @deprecated Use the `extension` getter/setter method instead.
* @param {Object} [options]
* @param {String} [options.store] - Store name. Default is the original extension.
* @returns {string} The extension eg.: `jpg` or if not found then an empty string ''
*/
FS.File.prototype.getExtension = function(options) {
var self = this;
return self.extension(options);
};
function checkContentType(fsFile, storeName, startOfType) {
var type;
if (storeName && fsFile.hasStored(storeName)) {
type = fsFile.type({store: storeName});
} else {
type = fsFile.type();
}
if (typeof type === "string") {
return type.indexOf(startOfType) === 0;
}
return false;
}
/**
* @method FS.File.prototype.isImage Is it an image file?
* @public
* @param {object} [options]
* @param {string} [options.store] The store we're interested in
*
* Returns true if the copy of this file in the specified store has an image
* content type. If the file object is unmounted or doesn't have a copy for
* the specified store, or if you don't specify a store, this method checks
* the content type of the original file.
*/
FS.File.prototype.isImage = function(options) {
return checkContentType(this, (options || {}).store, 'image/');
};
/**
* @method FS.File.prototype.isVideo Is it a video file?
* @public
* @param {object} [options]
* @param {string} [options.store] The store we're interested in
*
* Returns true if the copy of this file in the specified store has a video
* content type. If the file object is unmounted or doesn't have a copy for
* the specified store, or if you don't specify a store, this method checks
* the content type of the original file.
*/
FS.File.prototype.isVideo = function(options) {
return checkContentType(this, (options || {}).store, 'video/');
};
/**
* @method FS.File.prototype.isAudio Is it an audio file?
* @public
* @param {object} [options]
* @param {string} [options.store] The store we're interested in
*
* Returns true if the copy of this file in the specified store has an audio
* content type. If the file object is unmounted or doesn't have a copy for
* the specified store, or if you don't specify a store, this method checks
* the content type of the original file.
*/
FS.File.prototype.isAudio = function(options) {
return checkContentType(this, (options || {}).store, 'audio/');
};
/**
* @method FS.File.prototype.formattedSize
* @public
* @param {Object} options
* @param {String} [options.store=none,display original file size] Which file do you want to get the size of?
* @param {String} [options.formatString='0.00 b'] The `numeral` format string to use.
* @return {String} The file size formatted as a human readable string and reactively updated.
*
* * You must add the `numeral` package to your app before you can use this method.
* * If info is not found or a size can't be determined, it will show 0.
*/
FS.File.prototype.formattedSize = function fsFileFormattedSize(options) {
var self = this;
if (typeof numeral !== "function")
throw new Error("You must add the numeral package if you call FS.File.formattedSize");
options = options || {};
options = options.hash || options;
var size = self.size(options) || 0;
return numeral(size).format(options.formatString || '0.00 b');
};
/**
* @method FS.File.prototype.isUploaded Is this file completely uploaded?
* @public
* @returns {boolean} True if the number of uploaded bytes is equal to the file size.
*/
FS.File.prototype.isUploaded = function() {
var self = this;
// Make sure we use the updated file record
self.getFileRecord();
return !!self.uploadedAt;
};
/**
* @method FS.File.prototype.hasStored
* @public
* @param {string} storeName Name of the store
* @param {boolean} [optimistic=false] In case that the file record is not found, read below
* @returns {boolean} Is a version of this file stored in the given store?
*
* > Note: If the file is not published to the client or simply not found:
* this method cannot know for sure if it exists or not. The `optimistic`
* param is the boolean value to return. Are we `optimistic` that the copy
* could exist. This is the case in `FS.File.url` we are optimistic that the
* copy supplied by the user exists.
*/
FS.File.prototype.hasStored = function(storeName, optimistic) {
var self = this;
// Make sure we use the updated file record
self.getFileRecord();
// If we havent the published data then
if (FS.Utility.isEmpty(self.copies)) {
return !!optimistic;
}
if (typeof storeName === "string") {
// Return true only if the `key` property is present, which is not set until
// storage is complete.
return !!(self.copies && self.copies[storeName] && self.copies[storeName].key);
}
return false;
};
// Backwards compatibility
FS.File.prototype.hasCopy = FS.File.prototype.hasStored;
/**
* @method FS.File.prototype.getCopyInfo
* @public
* @deprecated Use individual methods with `store` option instead.
* @param {string} storeName Name of the store for which to get copy info.
* @returns {Object} The file details, e.g., name, size, key, etc., specific to the copy saved in this store.
*/
FS.File.prototype.getCopyInfo = function(storeName) {
var self = this;
// Make sure we use the updated file record
self.getFileRecord();
return (self.copies && self.copies[storeName]) || null;
};
/**
* @method FS.File.prototype._getInfo
* @private
* @param {String} [storeName] Name of the store for which to get file info. Omit for original file details.
* @param {Object} [options]
* @param {Boolean} [options.updateFileRecordFirst=false] Update this instance with data from the DB first?
* @returns {Object} The file details, e.g., name, size, key, etc. If not found, returns an empty object.
*/
FS.File.prototype._getInfo = function(storeName, options) {
var self = this;
options = options || {};
if (options.updateFileRecordFirst) {
// Make sure we use the updated file record
self.getFileRecord();
}
if (storeName) {
return (self.copies && self.copies[storeName]) || {};
} else {
return self.original || {};
}
};
/**
* @method FS.File.prototype._setInfo
* @private
* @param {String} storeName - Name of the store for which to set file info. Non-string will set original file details.
* @param {String} property - Property to set
* @param {String} value - New value for property
* @param {Boolean} save - Should the new value be saved to the DB, too, or just set in the FS.File properties?
* @returns {undefined}
*/
FS.File.prototype._setInfo = function(storeName, property, value, save) {
var self = this;
if (typeof storeName === "string") {
self.copies = self.copies || {};
self.copies[storeName] = self.copies[storeName] || {};
self.copies[storeName][property] = value;
save && self._saveChanges(storeName);
} else {
self.original = self.original || {};
self.original[property] = value;
save && self._saveChanges("_original");
}
};
/**
* @method FS.File.prototype.name
* @public
* @param {String|null} [value] - If setting the name, specify the new name as the first argument. Otherwise the options argument should be first.
* @param {Object} [options]
* @param {Object} [options.store=none,original] - Get or set the name of the version of the file that was saved in this store. Default is the original file name.
* @param {Boolean} [options.updateFileRecordFirst=false] Update this instance with data from the DB first? Applies to getter usage only.
* @param {Boolean} [options.save=true] Save change to database? Applies to setter usage only.
* @returns {String|undefined} If setting, returns `undefined`. If getting, returns the file name.
*/
FS.File.prototype.name = function(value, options) {
var self = this;
if (!options && ((typeof value === "object" && value !== null) || typeof value === "undefined")) {
// GET
options = value || {};
options = options.hash || options; // allow use as UI helper
return self._getInfo(options.store, options).name;
} else {
// SET
options = options || {};
return self._setInfo(options.store, 'name', value, typeof options.save === "boolean" ? options.save : true);
}
};
/**
* @method FS.File.prototype.extension
* @public
* @param {String|null} [value] - If setting the extension, specify the new extension (without period) as the first argument. Otherwise the options argument should be first.
* @param {Object} [options]
* @param {Object} [options.store=none,original] - Get or set the extension of the version of the file that was saved in this store. Default is the original file extension.
* @param {Boolean} [options.updateFileRecordFirst=false] Update this instance with data from the DB first? Applies to getter usage only.
* @param {Boolean} [options.save=true] Save change to database? Applies to setter usage only.
* @returns {String|undefined} If setting, returns `undefined`. If getting, returns the file extension or an empty string if there isn't one.
*/
FS.File.prototype.extension = function(value, options) {
var self = this;
if (!options && ((typeof value === "object" && value !== null) || typeof value === "undefined")) {
// GET
options = value || {};
return FS.Utility.getFileExtension(self.name(options) || '');
} else {
// SET
options = options || {};
var newName = FS.Utility.setFileExtension(self.name(options) || '', value);
return self._setInfo(options.store, 'name', newName, typeof options.save === "boolean" ? options.save : true);
}
};
/**
* @method FS.File.prototype.size
* @public
* @param {Number} [value] - If setting the size, specify the new size in bytes as the first argument. Otherwise the options argument should be first.
* @param {Object} [options]
* @param {Object} [options.store=none,original] - Get or set the size of the version of the file that was saved in this store. Default is the original file size.
* @param {Boolean} [options.updateFileRecordFirst=false] Update this instance with data from the DB first? Applies to getter usage only.
* @param {Boolean} [options.save=true] Save change to database? Applies to setter usage only.
* @returns {Number|undefined} If setting, returns `undefined`. If getting, returns the file size.
*/
FS.File.prototype.size = function(value, options) {
var self = this;
if (!options && ((typeof value === "object" && value !== null) || typeof value === "undefined")) {
// GET
options = value || {};
options = options.hash || options; // allow use as UI helper
return self._getInfo(options.store, options).size;
} else {
// SET
options = options || {};
return self._setInfo(options.store, 'size', value, typeof options.save === "boolean" ? options.save : true);
}
};
/**
* @method FS.File.prototype.type
* @public
* @param {String} [value] - If setting the type, specify the new type as the first argument. Otherwise the options argument should be first.
* @param {Object} [options]
* @param {Object} [options.store=none,original] - Get or set the type of the version of the file that was saved in this store. Default is the original file type.
* @param {Boolean} [options.updateFileRecordFirst=false] Update this instance with data from the DB first? Applies to getter usage only.
* @param {Boolean} [options.save=true] Save change to database? Applies to setter usage only.
* @returns {String|undefined} If setting, returns `undefined`. If getting, returns the file type.
*/
FS.File.prototype.type = function(value, options) {
var self = this;
if (!options && ((typeof value === "object" && value !== null) || typeof value === "undefined")) {
// GET
options = value || {};
options = options.hash || options; // allow use as UI helper
return self._getInfo(options.store, options).type;
} else {
// SET
options = options || {};
return self._setInfo(options.store, 'type', value, typeof options.save === "boolean" ? options.save : true);
}
};
/**
* @method FS.File.prototype.updatedAt
* @public
* @param {String} [value] - If setting updatedAt, specify the new date as the first argument. Otherwise the options argument should be first.
* @param {Object} [options]
* @param {Object} [options.store=none,original] - Get or set the last updated date for the version of the file that was saved in this store. Default is the original last updated date.
* @param {Boolean} [options.updateFileRecordFirst=false] Update this instance with data from the DB first? Applies to getter usage only.
* @param {Boolean} [options.save=true] Save change to database? Applies to setter usage only.
* @returns {String|undefined} If setting, returns `undefined`. If getting, returns the file's last updated date.
*/
FS.File.prototype.updatedAt = function(value, options) {
var self = this;
if (!options && ((typeof value === "object" && value !== null && !(value instanceof Date)) || typeof value === "undefined")) {
// GET
options = value || {};
options = options.hash || options; // allow use as UI helper
return self._getInfo(options.store, options).updatedAt;
} else {
// SET
options = options || {};
return self._setInfo(options.store, 'updatedAt', value, typeof options.save === "boolean" ? options.save : true);
}
};
/**
* @method FS.File.onStoredCallback
* @summary Calls callback when the file is fully stored to the specify storeName
* @public
* @param {String} [storeName] - The name of the file store we want to get called when stored.
* @param {function} [callback]
*/
FS.File.prototype.onStoredCallback = function (storeName, callback) {
// Check file is not already stored
if (this.hasStored(storeName)) {
callback();
return;
}
if (Meteor.isServer) {
// Listen to file stored events
// TODO Require thinking whether it is better to use observer for case of using multiple application instances, Ask for same image url while upload is being done.
this.on('stored', function (newStoreName) {
// If stored is completed to the specified store call callback
if (storeName === newStoreName) {
// Remove the specified file stored listener
this.removeListener('stored', arguments.callee);
callback();
}
}.bind(this)
);
} else {
var fileId = this._id,
collectionName = this.collectionName;
// Wait for file to be fully uploaded
Tracker.autorun(function (c) {
Meteor.call('_cfs_returnWhenStored', collectionName, fileId, storeName, function (error, result) {
if (result && result === true) {
c.stop();
callback();
} else {
Meteor.setTimeout(function () {
c.invalidate();
}, 100);
}
});
});
}
};
/**
* @method FS.File.onStored
* @summary Function that returns when the file is fully stored to the specify storeName
* @public
* @param {String} storeName - The name of the file store we want to get called when stored.
*
* Function that returns when the file is fully stored to the specify storeName.
*
* For example needed if wanted to save the direct link to a file on s3 when fully uploaded.
*/
FS.File.prototype.onStored = function (arguments) {
var onStoredSync = Meteor.wrapAsync(this.onStoredCallback);
return onStoredSync.call(this, arguments);
};
function isBasicObject(obj) {
return (obj === Object(obj) && Object.getPrototypeOf(obj) === Object.prototype);
}
// getPrototypeOf polyfill
if (typeof Object.getPrototypeOf !== "function") {
if (typeof "".__proto__ === "object") {
Object.getPrototypeOf = function(object) {
return object.__proto__;
};
} else {
Object.getPrototypeOf = function(object) {
// May break if the constructor has been tampered with
return object.constructor.prototype;
};
}
}

View file

@ -0,0 +1,361 @@
/**
* Notes a details about a storage adapter failure within the file record
* @param {string} storeName
* @param {number} maxTries
* @return {undefined}
* @todo deprecate this
*/
FS.File.prototype.logCopyFailure = function(storeName, maxTries) {
var self = this;
// hasStored will update from the fileRecord
if (self.hasStored(storeName)) {
throw new Error("logCopyFailure: invalid storeName");
}
// Make sure we have a temporary file saved since we will be
// trying the save again.
FS.TempStore.ensureForFile(self);
var now = new Date();
var currentCount = (self.failures && self.failures.copies && self.failures.copies[storeName] && typeof self.failures.copies[storeName].count === "number") ? self.failures.copies[storeName].count : 0;
maxTries = maxTries || 5;
var modifier = {};
modifier.$set = {};
modifier.$set['failures.copies.' + storeName + '.lastAttempt'] = now;
if (currentCount === 0) {
modifier.$set['failures.copies.' + storeName + '.firstAttempt'] = now;
}
modifier.$set['failures.copies.' + storeName + '.count'] = currentCount + 1;
modifier.$set['failures.copies.' + storeName + '.doneTrying'] = (currentCount + 1 >= maxTries);
self.update(modifier);
};
/**
* Has this store permanently failed?
* @param {String} storeName The name of the store
* @return {boolean} Has this store failed permanently?
* @todo deprecate this
*/
FS.File.prototype.failedPermanently = function(storeName) {
var self = this;
return !!(self.failures &&
self.failures.copies &&
self.failures.copies[storeName] &&
self.failures.copies[storeName].doneTrying);
};
/**
* @method FS.File.prototype.createReadStream
* @public
* @param {String} [storeName]
* @returns {stream.Readable} Readable NodeJS stream
*
* Returns a readable stream. Where the stream reads from depends on the FS.File instance and whether you pass a store name.
*
* * If you pass a `storeName`, a readable stream for the file data saved in that store is returned.
* * If you don't pass a `storeName` and data is attached to the FS.File instance (on `data` property, which must be a DataMan instance), then a readable stream for the attached data is returned.
* * If you don't pass a `storeName` and there is no data attached to the FS.File instance, a readable stream for the file data currently in the temporary store (`FS.TempStore`) is returned.
*
*/
FS.File.prototype.createReadStream = function(storeName) {
var self = this;
// If we dont have a store name but got Buffer data?
if (!storeName && self.data) {
FS.debug && console.log("fileObj.createReadStream creating read stream for attached data");
// Stream from attached data if present
return self.data.createReadStream();
} else if (!storeName && FS.TempStore && FS.TempStore.exists(self)) {
FS.debug && console.log("fileObj.createReadStream creating read stream for temp store");
// Stream from temp store - its a bit slower than regular streams?
return FS.TempStore.createReadStream(self);
} else {
// Stream from the store using storage adapter
if (self.isMounted()) {
var storage = self.collection.storesLookup[storeName] || self.collection.primaryStore;
FS.debug && console.log("fileObj.createReadStream creating read stream for store", storage.name);
// return stream
return storage.adapter.createReadStream(self);
} else {
throw new Meteor.Error('File not mounted');
}
}
};
/**
* @method FS.File.prototype.createWriteStream
* @public
* @param {String} [storeName]
* @returns {stream.Writeable} Writeable NodeJS stream
*
* Returns a writeable stream. Where the stream writes to depends on whether you pass in a store name.
*
* * If you pass a `storeName`, a writeable stream for (over)writing the file data in that store is returned.
* * If you don't pass a `storeName`, a writeable stream for writing to the temp store for this file is returned.
*
*/
FS.File.prototype.createWriteStream = function(storeName) {
var self = this;
// We have to have a mounted file in order for this to work
if (self.isMounted()) {
if (!storeName && FS.TempStore && FS.FileWorker) {
// If we have worker installed - we pass the file to FS.TempStore
// We dont need the storeName since all stores will be generated from
// TempStore.
// This should trigger FS.FileWorker at some point?
FS.TempStore.createWriteStream(self);
} else {
// Stream directly to the store using storage adapter
var storage = self.collection.storesLookup[storeName] || self.collection.primaryStore;
return storage.adapter.createWriteStream(self);
}
} else {
throw new Meteor.Error('File not mounted');
}
};
/**
* @method FS.File.prototype.copy Makes a copy of the file and underlying data in all stores.
* @public
* @returns {FS.File} The new FS.File instance
*/
FS.File.prototype.copy = function() {
var self = this;
if (!self.isMounted()) {
throw new Error("Cannot copy a file that is not associated with a collection");
}
// Get the file record
var fileRecord = self.collection.files.findOne({_id: self._id}, {transform: null}) || {};
// Remove _id and copy keys from the file record
delete fileRecord._id;
// Insert directly; we don't have access to "original" in this case
var newId = self.collection.files.insert(fileRecord);
var newFile = self.collection.findOne(newId);
// Copy underlying files in the stores
var mod, oldKey;
for (var name in newFile.copies) {
if (newFile.copies.hasOwnProperty(name)) {
oldKey = newFile.copies[name].key;
if (oldKey) {
// We need to ask the adapter for the true oldKey because
// right now gridfs does some extra stuff.
// TODO GridFS should probably set the full key object
// (with _id and filename) into `copies.key`
// so that copies.key can be passed directly to
// createReadStreamForFileKey
var sourceFileStorage = self.collection.storesLookup[name];
if (!sourceFileStorage) {
throw new Error(name + " is not a valid store name");
}
oldKey = sourceFileStorage.adapter.fileKey(self);
// delete so that new fileKey will be generated in copyStoreData
delete newFile.copies[name].key;
mod = mod || {};
mod["copies." + name + ".key"] = copyStoreData(newFile, name, oldKey);
}
}
}
// Update keys in the filerecord
if (mod) {
newFile.update({$set: mod});
}
return newFile;
};
Meteor.methods({
// Does a HEAD request to URL to get the type, updatedAt,
// and size prior to actually downloading the data.
// That way we can do filter checks without actually downloading.
'_cfs_getUrlInfo': function (url, options) {
check(url, String);
check(options, Object);
this.unblock();
var response = HTTP.call("HEAD", url, options);
var headers = response.headers;
var result = {};
if (headers['content-type']) {
result.type = headers['content-type'];
}
if (headers['content-length']) {
result.size = +headers['content-length'];
}
if (headers['last-modified']) {
result.updatedAt = new Date(headers['last-modified']);
}
return result;
},
// Helper function that checks whether given fileId from collectionName
// Is fully uploaded to specify storeName.
'_cfs_returnWhenStored' : function (collectionName, fileId, storeName) {
check(collectionName, String);
check(fileId, String);
check(storeName, String);
var collection = FS._collections[collectionName];
if (!collection) {
return Meteor.Error('_cfs_returnWhenStored: FSCollection name not exists');
}
var file = collection.findOne({_id: fileId});
if (!file) {
return Meteor.Error('_cfs_returnWhenStored: FSFile not exists');
}
return file.hasStored(storeName);
}
});
// TODO maybe this should be in cfs-storage-adapter
function _copyStoreData(fileObj, storeName, sourceKey, callback) {
if (!fileObj.isMounted()) {
throw new Error("Cannot copy store data for a file that is not associated with a collection");
}
var storage = fileObj.collection.storesLookup[storeName];
if (!storage) {
throw new Error(storeName + " is not a valid store name");
}
// We want to prevent beforeWrite and transformWrite from running, so
// we interact directly with the store.
var destinationKey = storage.adapter.fileKey(fileObj);
var readStream = storage.adapter.createReadStreamForFileKey(sourceKey);
var writeStream = storage.adapter.createWriteStreamForFileKey(destinationKey);
writeStream.once('stored', function(result) {
callback(null, result.fileKey);
});
writeStream.once('error', function(error) {
callback(error);
});
readStream.pipe(writeStream);
}
var copyStoreData = Meteor.wrapAsync(_copyStoreData);
/**
* @method FS.File.prototype.copyData Copies the content of a store directly into another store.
* @public
* @param {string} sourceStoreName
* @param {string} targetStoreName
* @param {boolean=} move
*/
FS.File.prototype.copyData = function(sourceStoreName, targetStoreName, move){
move = !!move;
/**
* @type {Object.<string,*>}
*/
var sourceStoreValues = this.copies[sourceStoreName];
/**
* @type {string}
*/
var copyKey = cloneDataToStore(this, sourceStoreName, targetStoreName, move);
/**
* @type {Object.<string,*>}
*/
var targetStoreValues = {};
for (var v in sourceStoreValues) {
if (sourceStoreValues.hasOwnProperty(v)) {
targetStoreValues[v] = sourceStoreValues[v]
}
}
targetStoreValues.key = copyKey;
targetStoreValues.createdAt = new Date();
targetStoreValues.updatedAt = new Date();
/**
*
* @type {modifier}
*/
var modifier = {};
modifier.$set = {};
modifier.$set["copies."+targetStoreName] = targetStoreValues;
if(move){
modifier.$unset = {};
modifier.$unset["copies."+sourceStoreName] = "";
}
this.update(modifier);
};
/**
* @method FS.File.prototype.moveData Moves the content of a store directly into another store.
* @public
* @param {string} sourceStoreName
* @param {string} targetStoreName
*/
FS.File.prototype.moveData = function(sourceStoreName, targetStoreName){
this.copyData(sourceStoreName, targetStoreName, true);
};
// TODO maybe this should be in cfs-storage-adapter
/**
*
* @param {FS.File} fileObj
* @param {string} sourceStoreName
* @param {string} targetStoreName
* @param {boolean} move
* @param callback
* @private
*/
function _copyDataFromStoreToStore(fileObj, sourceStoreName, targetStoreName, move, callback) {
if (!fileObj.isMounted()) {
throw new Error("Cannot copy store data for a file that is not associated with a collection");
}
/**
* @type {FS.StorageAdapter}
*/
var sourceStorage = fileObj.collection.storesLookup[sourceStoreName];
/**
* @type {FS.StorageAdapter}
*/
var targetStorage = fileObj.collection.storesLookup[targetStoreName];
if (!sourceStorage) {
throw new Error(sourceStoreName + " is not a valid store name");
}
if (!targetStorage) {
throw new Error(targetStorage + " is not a valid store name");
}
// We want to prevent beforeWrite and transformWrite from running, so
// we interact directly with the store.
var sourceKey = sourceStorage.adapter.fileKey(fileObj);
var targetKey = targetStorage.adapter.fileKey(fileObj);
var readStream = sourceStorage.adapter.createReadStreamForFileKey(sourceKey);
var writeStream = targetStorage.adapter.createWriteStreamForFileKey(targetKey);
writeStream.safeOnce('stored', function(result) {
if(move && sourceStorage.adapter.remove(fileObj)===false){
callback("Copied to store:" + targetStoreName
+ " with fileKey: "
+ result.fileKey
+ ", but could not delete from source store: "
+ sourceStoreName);
}else{
callback(null, result.fileKey);
}
});
writeStream.once('error', function(error) {
callback(error);
});
readStream.pipe(writeStream);
}
var cloneDataToStore = Meteor.wrapAsync(_copyDataFromStoreToStore);

View file

@ -0,0 +1,749 @@
## Public and Private API ##
_API documentation automatically generated by [docmeteor](https://github.com/raix/docmeteor)._
***
__File: ["fsFile-common.js"](fsFile-common.js) Where: {client|server}__
***
### <a name="FS.File"></a>new *fs*.File([ref])&nbsp;&nbsp;<sub><i>Anywhere</i></sub> ###
*This method __File__ is defined in `FS`*
__Arguments__
* __ref__ *{object|[FS.File](#FS.File)|[data to attach](#data to attach)}* (Optional)
Another FS.File instance, a filerecord, or some data to pass to attachData
> ```FS.File = function(ref, createdByTransform) { ...``` [fsFile-common.js:8](fsFile-common.js#L8)
-
### <a name="FS.File.prototype.attachData"></a>*fsFile*.attachData(data, [options], [callback])&nbsp;&nbsp;<sub><i>Anywhere</i></sub> ###
*This method __attachData__ is defined in `prototype` of `FS.File`*
__Arguments__
* __data__ *{[File](#File)|[Blob](#Blob)|Buffer|ArrayBuffer|Uint8Array|String}*
The data that you want to attach to the file.
* __options__ *{Object}* (Optional)
Options
* __type__ *{String}* (Optional)
The data content (MIME) type, if known.
* __headers__ *{String}* (Optional)
When attaching a URL, headers to be used for the GET request (currently server only)
* __auth__ *{String}* (Optional)
When attaching a URL, "username:password" to be used for the GET request (currently server only)
* __callback__ *{Function}* (Optional)
Callback function, callback(error). On the client, a callback is required if data is a URL.
__Returns__ *{FS.File}*
This FS.File instance.
> ```FS.File.prototype.attachData = function fsFileAttachData(data, options, callback) { ...``` [fsFile-common.js:36](fsFile-common.js#L36)
-
### <a name="FS.File.prototype.uploadProgress"></a>*fsFile*.uploadProgress()&nbsp;&nbsp;<sub><i>Anywhere</i></sub> ###
*This method __uploadProgress__ is defined in `prototype` of `FS.File`*
__Returns__ *{number}*
The server confirmed upload progress
> ```FS.File.prototype.uploadProgress = function() { ...``` [fsFile-common.js:154](fsFile-common.js#L154)
-
### <a name="FS.File.prototype.controlledByDeps"></a>*fsFile*.controlledByDeps()&nbsp;&nbsp;<sub><i>Anywhere</i></sub> ###
*This method __controlledByDeps__ is defined in `prototype` of `FS.File`*
__Returns__ *{FS.Collection}*
Returns true if this FS.File is reactive
> Note: Returns true if this FS.File object was created by a FS.Collection
> and we are in a reactive computations. What does this mean? Well it should
> mean that our fileRecord is fully updated by Meteor and we are mounted on
> a collection
> ```FS.File.prototype.controlledByDeps = function() { ...``` [fsFile-common.js:179](fsFile-common.js#L179)
-
### <a name="FS.File.prototype.getCollection"></a>*fsFile*.getCollection()&nbsp;&nbsp;<sub><i>Anywhere</i></sub> ###
*This method __getCollection__ is defined in `prototype` of `FS.File`*
__Returns__ *{FS.Collection}*
Returns attached collection or undefined if not mounted
> ```FS.File.prototype.getCollection = function() { ...``` [fsFile-common.js:189](fsFile-common.js#L189)
-
### <a name="FS.File.prototype.isMounted"></a>*fsFile*.isMounted()&nbsp;&nbsp;<sub><i>Anywhere</i></sub> ###
*This method __isMounted__ is defined in `prototype` of `FS.File`*
__Returns__ *{FS.Collection}*
Returns attached collection or undefined if not mounted
> ```FS.File.prototype.isMounted = FS.File.prototype.getCollection;``` [fsFile-common.js:217](fsFile-common.js#L217)
-
### <a name="FS.File.prototype.getFileRecord"></a>*fsFile*.getFileRecord()&nbsp;&nbsp;<sub><i>Anywhere</i></sub> ###
*This method __getFileRecord__ is defined in `prototype` of `FS.File`*
__Returns__ *{object}*
The filerecord
> ```FS.File.prototype.getFileRecord = function() { ...``` [fsFile-common.js:224](fsFile-common.js#L224)
-
### <a name="FS.File.prototype.update"></a>*fsFile*.update(modifier, [options], [callback])&nbsp;&nbsp;<sub><i>Anywhere</i></sub> ###
*This method __update__ is defined in `prototype` of `FS.File`*
__Arguments__
* __modifier__ *{[modifier](#modifier)}*
* __options__ *{object}* (Optional)
* __callback__ *{function}* (Optional)
Updates the fileRecord.
> ```FS.File.prototype.update = function(modifier, options, callback) { ...``` [fsFile-common.js:255](fsFile-common.js#L255)
-
### <a name="FS.File.prototype._saveChanges"></a>*fsFile*._saveChanges([what])&nbsp;&nbsp;<sub><i>Anywhere</i></sub> ###
*This method is private*
*This method ___saveChanges__ is defined in `prototype` of `FS.File`*
__Arguments__
* __what__ *{String}* (Optional)
"_original" to save original info, or a store name to save info for that store, or saves everything
Updates the fileRecord from values currently set on the FS.File instance.
> ```FS.File.prototype._saveChanges = function(what) { ...``` [fsFile-common.js:290](fsFile-common.js#L290)
-
### <a name="FS.File.prototype.remove"></a>*fsFile*.remove([callback])&nbsp;&nbsp;<sub><i>Anywhere</i></sub> ###
*This method __remove__ is defined in `prototype` of `FS.File`*
__Arguments__
* __callback__ *{Function}* (Optional)
__Returns__ *{number}*
Count
Remove the current file from its FS.Collection
> ```FS.File.prototype.remove = function(callback) { ...``` [fsFile-common.js:323](fsFile-common.js#L323)
-
### <a name="FS.File.prototype.moveTo"></a>*fsFile*.moveTo(targetCollection)&nbsp;&nbsp;<sub><i>Anywhere</i></sub> ###
*This method is private*
*This method __moveTo__ is defined in `prototype` of `FS.File`*
__Arguments__
* __targetCollection__ *{[FS.Collection](#FS.Collection)}*
__TODO__
```
* Needs to be implemented
```
Move the file from current collection to another collection
> Note: Not yet implemented
> ```FS.File.prototype.getExtension = function(options) { ...``` [fsFile-common.js:364](fsFile-common.js#L364)
-
### <a name="FS.File.prototype.getExtension"></a>*fsFile*.getExtension([options])&nbsp;&nbsp;<sub><i>Anywhere</i></sub> ###
> __Warning!__
> This method "FS.File.prototype.getExtension" has deprecated from the API
> Use the `extension` getter/setter method instead.
*This method __getExtension__ is defined in `prototype` of `FS.File`*
__Arguments__
* __options__ *{Object}* (Optional)
* __store__ *{String}* (Optional)
Store name. Default is the original extension.
__Returns__ *{string}*
The extension eg.: `jpg` or if not found then an empty string ''
> ```FS.File.prototype.getExtension = function(options) { ...``` [fsFile-common.js:364](fsFile-common.js#L364)
-
### <a name="FS.File.prototype.isImage"></a>*fsFile*.isImage([options])&nbsp;&nbsp;<sub><i>Anywhere</i></sub> ###
*This method __isImage__ is defined in `prototype` of `FS.File`*
__Arguments__
* __options__ *{object}* (Optional)
* __store__ *{string}* (Optional)
The store we're interested in
Returns true if the copy of this file in the specified store has an image
content type. If the file object is unmounted or doesn't have a copy for
the specified store, or if you don't specify a store, this method checks
the content type of the original file.
> ```FS.File.prototype.isImage = function(options) { ...``` [fsFile-common.js:393](fsFile-common.js#L393)
-
### <a name="FS.File.prototype.isVideo"></a>*fsFile*.isVideo([options])&nbsp;&nbsp;<sub><i>Anywhere</i></sub> ###
*This method __isVideo__ is defined in `prototype` of `FS.File`*
__Arguments__
* __options__ *{object}* (Optional)
* __store__ *{string}* (Optional)
The store we're interested in
Returns true if the copy of this file in the specified store has a video
content type. If the file object is unmounted or doesn't have a copy for
the specified store, or if you don't specify a store, this method checks
the content type of the original file.
> ```FS.File.prototype.isVideo = function(options) { ...``` [fsFile-common.js:408](fsFile-common.js#L408)
-
### <a name="FS.File.prototype.isAudio"></a>*fsFile*.isAudio([options])&nbsp;&nbsp;<sub><i>Anywhere</i></sub> ###
*This method __isAudio__ is defined in `prototype` of `FS.File`*
__Arguments__
* __options__ *{object}* (Optional)
* __store__ *{string}* (Optional)
The store we're interested in
Returns true if the copy of this file in the specified store has an audio
content type. If the file object is unmounted or doesn't have a copy for
the specified store, or if you don't specify a store, this method checks
the content type of the original file.
> ```FS.File.prototype.isAudio = function(options) { ...``` [fsFile-common.js:423](fsFile-common.js#L423)
-
### <a name="FS.File.prototype.formattedSize"></a>*fsFile*.formattedSize({Object}, {String}, {String})&nbsp;&nbsp;<sub><i>Anywhere</i></sub> ###
*This method __formattedSize__ is defined in `prototype` of `FS.File`*
__Arguments__
* __{Object}__ *{any}*
options
* __{String}__ *{any}*
[options.store=none,display original file size] Which file do you want to get the size of?
* __{String}__ *{any}*
[options.formatString='0.00 b'] The `numeral` format string to use.
__Returns__ *{String}*
The file size formatted as a human readable string and reactively updated.
You must add the `numeral` package to your app before you can use this method.
If info is not found or a size can't be determined, it will show 0.
> ```FS.File.prototype.formattedSize = function fsFileFormattedSize(options) { ...``` [fsFile-common.js:438](fsFile-common.js#L438)
-
### <a name="FS.File.prototype.isUploaded"></a>*fsFile*.isUploaded()&nbsp;&nbsp;<sub><i>Anywhere</i></sub> ###
*This method __isUploaded__ is defined in `prototype` of `FS.File`*
__Returns__ *{boolean}*
True if the number of uploaded bytes is equal to the file size.
> ```FS.File.prototype.isUploaded = function() { ...``` [fsFile-common.js:456](fsFile-common.js#L456)
-
### <a name="FS.File.prototype.hasStored"></a>*fsFile*.hasStored(storeName, [optimistic])&nbsp;&nbsp;<sub><i>Anywhere</i></sub> ###
*This method __hasStored__ is defined in `prototype` of `FS.File`*
__Arguments__
* __storeName__ *{string}*
Name of the store
* __optimistic__ *{boolean}* (Optional, Default = false)
In case that the file record is not found, read below
__Returns__ *{boolean}*
Is a version of this file stored in the given store?
> Note: If the file is not published to the client or simply not found:
this method cannot know for sure if it exists or not. The `optimistic`
param is the boolean value to return. Are we `optimistic` that the copy
could exist. This is the case in `FS.File.url` we are optimistic that the
copy supplied by the user exists.
> ```FS.File.prototype.hasStored = function(storeName, optimistic) { ...``` [fsFile-common.js:478](fsFile-common.js#L478)
-
### <a name="FS.File.prototype.getCopyInfo"></a>*fsFile*.getCopyInfo(storeName)&nbsp;&nbsp;<sub><i>Anywhere</i></sub> ###
> __Warning!__
> This method "FS.File.prototype.getCopyInfo" has deprecated from the API
> Use individual methods with `store` option instead.
*This method __getCopyInfo__ is defined in `prototype` of `FS.File`*
__Arguments__
* __storeName__ *{string}*
Name of the store for which to get copy info.
__Returns__ *{Object}*
The file details, e.g., name, size, key, etc., specific to the copy saved in this store.
> ```FS.File.prototype.getCopyInfo = function(storeName) { ...``` [fsFile-common.js:504](fsFile-common.js#L504)
-
### <a name="FS.File.prototype._getInfo"></a>*fsFile*._getInfo([storeName], [options])&nbsp;&nbsp;<sub><i>Anywhere</i></sub> ###
*This method is private*
*This method ___getInfo__ is defined in `prototype` of `FS.File`*
__Arguments__
* __storeName__ *{String}* (Optional)
Name of the store for which to get file info. Omit for original file details.
* __options__ *{Object}* (Optional)
* __updateFileRecordFirst__ *{Boolean}* (Optional, Default = false)
Update this instance with data from the DB first?
__Returns__ *{Object}*
The file details, e.g., name, size, key, etc. If not found, returns an empty object.
> ```FS.File.prototype._getInfo = function(storeName, options) { ...``` [fsFile-common.js:519](fsFile-common.js#L519)
-
### <a name="FS.File.prototype._setInfo"></a>*fsFile*._setInfo(storeName, property, value, save)&nbsp;&nbsp;<sub><i>Anywhere</i></sub> ###
*This method is private*
*This method ___setInfo__ is defined in `prototype` of `FS.File`*
__Arguments__
* __storeName__ *{String}*
Name of the store for which to set file info. Non-string will set original file details.
* __property__ *{String}*
Property to set
* __value__ *{String}*
New value for property
* __save__ *{Boolean}*
Should the new value be saved to the DB, too, or just set in the FS.File properties?
__Returns__ *{undefined}*
> ```FS.File.prototype._setInfo = function(storeName, property, value, save) { ...``` [fsFile-common.js:544](fsFile-common.js#L544)
-
### <a name="FS.File.prototype.name"></a>*fsFile*.name([value], [options])&nbsp;&nbsp;<sub><i>Anywhere</i></sub> ###
*This method __name__ is defined in `prototype` of `FS.File`*
__Arguments__
* __value__ *{String|null}* (Optional)
If setting the name, specify the new name as the first argument. Otherwise the options argument should be first.
* __options__ *{Object}* (Optional)
* __store__ *{Object}* (Optional, Default = none,original)
Get or set the name of the version of the file that was saved in this store. Default is the original file name.
* __updateFileRecordFirst__ *{Boolean}* (Optional, Default = false)
Update this instance with data from the DB first? Applies to getter usage only.
* __save__ *{Boolean}* (Optional, Default = true)
Save change to database? Applies to setter usage only.
__Returns__ *{String|undefined}*
If setting, returns `undefined`. If getting, returns the file name.
> ```FS.File.prototype.name = function(value, options) { ...``` [fsFile-common.js:568](fsFile-common.js#L568)
-
### <a name="FS.File.prototype.extension"></a>*fsFile*.extension([value], [options])&nbsp;&nbsp;<sub><i>Anywhere</i></sub> ###
*This method __extension__ is defined in `prototype` of `FS.File`*
__Arguments__
* __value__ *{String|null}* (Optional)
If setting the extension, specify the new extension (without period) as the first argument. Otherwise the options argument should be first.
* __options__ *{Object}* (Optional)
* __store__ *{Object}* (Optional, Default = none,original)
Get or set the extension of the version of the file that was saved in this store. Default is the original file extension.
* __updateFileRecordFirst__ *{Boolean}* (Optional, Default = false)
Update this instance with data from the DB first? Applies to getter usage only.
* __save__ *{Boolean}* (Optional, Default = true)
Save change to database? Applies to setter usage only.
__Returns__ *{String|undefined}*
If setting, returns `undefined`. If getting, returns the file extension or an empty string if there isn't one.
> ```FS.File.prototype.extension = function(value, options) { ...``` [fsFile-common.js:593](fsFile-common.js#L593)
-
### <a name="FS.File.prototype.size"></a>*fsFile*.size([value], [options])&nbsp;&nbsp;<sub><i>Anywhere</i></sub> ###
*This method __size__ is defined in `prototype` of `FS.File`*
__Arguments__
* __value__ *{Number}* (Optional)
If setting the size, specify the new size in bytes as the first argument. Otherwise the options argument should be first.
* __options__ *{Object}* (Optional)
* __store__ *{Object}* (Optional, Default = none,original)
Get or set the size of the version of the file that was saved in this store. Default is the original file size.
* __updateFileRecordFirst__ *{Boolean}* (Optional, Default = false)
Update this instance with data from the DB first? Applies to getter usage only.
* __save__ *{Boolean}* (Optional, Default = true)
Save change to database? Applies to setter usage only.
__Returns__ *{Number|undefined}*
If setting, returns `undefined`. If getting, returns the file size.
> ```FS.File.prototype.size = function(value, options) { ...``` [fsFile-common.js:618](fsFile-common.js#L618)
-
### <a name="FS.File.prototype.type"></a>*fsFile*.type([value], [options])&nbsp;&nbsp;<sub><i>Anywhere</i></sub> ###
*This method __type__ is defined in `prototype` of `FS.File`*
__Arguments__
* __value__ *{String}* (Optional)
If setting the type, specify the new type as the first argument. Otherwise the options argument should be first.
* __options__ *{Object}* (Optional)
* __store__ *{Object}* (Optional, Default = none,original)
Get or set the type of the version of the file that was saved in this store. Default is the original file type.
* __updateFileRecordFirst__ *{Boolean}* (Optional, Default = false)
Update this instance with data from the DB first? Applies to getter usage only.
* __save__ *{Boolean}* (Optional, Default = true)
Save change to database? Applies to setter usage only.
__Returns__ *{String|undefined}*
If setting, returns `undefined`. If getting, returns the file type.
> ```FS.File.prototype.type = function(value, options) { ...``` [fsFile-common.js:643](fsFile-common.js#L643)
-
### <a name="FS.File.prototype.updatedAt"></a>*fsFile*.updatedAt([value], [options])&nbsp;&nbsp;<sub><i>Anywhere</i></sub> ###
*This method __updatedAt__ is defined in `prototype` of `FS.File`*
__Arguments__
* __value__ *{String}* (Optional)
If setting updatedAt, specify the new date as the first argument. Otherwise the options argument should be first.
* __options__ *{Object}* (Optional)
* __store__ *{Object}* (Optional, Default = none,original)
Get or set the last updated date for the version of the file that was saved in this store. Default is the original last updated date.
* __updateFileRecordFirst__ *{Boolean}* (Optional, Default = false)
Update this instance with data from the DB first? Applies to getter usage only.
* __save__ *{Boolean}* (Optional, Default = true)
Save change to database? Applies to setter usage only.
__Returns__ *{String|undefined}*
If setting, returns `undefined`. If getting, returns the file's last updated date.
> ```FS.File.prototype.updatedAt = function(value, options) { ...``` [fsFile-common.js:668](fsFile-common.js#L668)
***
__File: ["fsFile-server.js"](fsFile-server.js) Where: {server}__
***
### <a name="FS.File.prototype.logCopyFailure"></a>*fsFile*.logCopyFailure(storeName, maxTries)&nbsp;&nbsp;<sub><i>Server</i></sub> ###
```
Notes a details about a storage adapter failure within the file record
```
*This method __logCopyFailure__ is defined in `prototype` of `FS.File`*
__Arguments__
* __storeName__ *{string}*
* __maxTries__ *{number}*
__Returns__ *{undefined}*
__TODO__
```
* deprecate this
```
> ```FS.File.prototype.logCopyFailure = function(storeName, maxTries) { ...``` [fsFile-server.js:8](fsFile-server.js#L8)
-
### <a name="FS.File.prototype.failedPermanently"></a>*fsFile*.failedPermanently(storeName)&nbsp;&nbsp;<sub><i>Server</i></sub> ###
```
Has this store permanently failed?
```
*This method __failedPermanently__ is defined in `prototype` of `FS.File`*
__Arguments__
* __storeName__ *{String}*
The name of the store
__Returns__ *{boolean}*
Has this store failed permanently?
__TODO__
```
* deprecate this
```
> ```FS.File.prototype.failedPermanently = function(storeName) { ...``` [fsFile-server.js:41](fsFile-server.js#L41)
-
### <a name="FS.File.prototype.createReadStream"></a>*fsFile*.createReadStream([storeName])&nbsp;&nbsp;<sub><i>Server</i></sub> ###
*This method __createReadStream__ is defined in `prototype` of `FS.File`*
__Arguments__
* __storeName__ *{String}* (Optional)
__Returns__ *{stream.Readable}*
Readable NodeJS stream
Returns a readable stream. Where the stream reads from depends on the FS.File instance and whether you pass a store name.
If you pass a `storeName`, a readable stream for the file data saved in that store is returned.
If you don't pass a `storeName` and data is attached to the FS.File instance (on `data` property, which must be a DataMan instance), then a readable stream for the attached data is returned.
If you don't pass a `storeName` and there is no data attached to the FS.File instance, a readable stream for the file data currently in the temporary store (`FS.TempStore`) is returned.
> ```FS.File.prototype.createReadStream = function(storeName) { ...``` [fsFile-server.js:62](fsFile-server.js#L62)
-
### <a name="FS.File.prototype.createWriteStream"></a>*fsFile*.createWriteStream([storeName])&nbsp;&nbsp;<sub><i>Server</i></sub> ###
*This method __createWriteStream__ is defined in `prototype` of `FS.File`*
__Arguments__
* __storeName__ *{String}* (Optional)
__Returns__ *{stream.Writeable}*
Writeable NodeJS stream
Returns a writeable stream. Where the stream writes to depends on whether you pass in a store name.
If you pass a `storeName`, a writeable stream for (over)writing the file data in that store is returned.
If you don't pass a `storeName`, a writeable stream for writing to the temp store for this file is returned.
> ```FS.File.prototype.createWriteStream = function(storeName) { ...``` [fsFile-server.js:100](fsFile-server.js#L100)
-
### <a name="FS.File.prototype.copy"></a>*fsFile*.copy()&nbsp;&nbsp;<sub><i>Server</i></sub> ###
*This method __copy__ is defined in `prototype` of `FS.File`*
__Returns__ *{FS.File}*
The new FS.File instance
> ```FS.File.prototype.copy = function() { ...``` [fsFile-server.js:126](fsFile-server.js#L126)

View file

@ -0,0 +1,55 @@
Package.describe({
git: 'https://github.com/zcfs/Meteor-cfs-file.git',
name: 'wekan-cfs-file',
version: '0.1.17',
summary: 'CollectionFS, FS.File object'
});
Npm.depends({
temp: "0.7.0" // for tests only
});
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@0.0.30'
]);
api.use([
'wekan-cfs-base-package@0.0.30',
'wekan-cfs-storage-adapter@0.2.1',
'tracker',
'check',
'ddp',
'mongo',
'http',
'wekan-cfs-data-man@0.0.6',
'raix:eventemitter@0.1.1'
]);
api.addFiles([
'fsFile-common.js'
], 'client');
api.addFiles([
'fsFile-common.js',
'fsFile-server.js'
], 'server');
});
Package.onTest(function (api) {
api.use([
'wekan-cfs-standard-packages@0.0.0',
'wekan-cfs-gridfs@0.0.0',
'tinytest@1.0.0',
'http@1.0.0',
'test-helpers@1.0.0',
'wekan-cfs-http-methods@0.0.29'
]);
api.addFiles([
'tests/file-tests.js'
]);
});

View file

@ -0,0 +1,436 @@
function bin2str(bufView) {
var length = bufView.length;
var result = '';
for (var i = 0; i<length; i+=65535) {
var addition = 65535;
if(i + 65535 > length) {
addition = length - i;
}
try {
// this fails on phantomjs due to old webkit bug; hence the try/catch
result += String.fromCharCode.apply(null, bufView.subarray(i,i+addition));
} catch (e) {
var dataArray = [];
for (var j = i; j < i+addition; j++) {
dataArray.push(bufView[j]);
}
result += String.fromCharCode.apply(null, dataArray);
}
}
return result;
}
//function ab2str(buffer) {
// return bin2str(new Uint8Array(buffer));
//}
function str2ab(str) {
var buf = new ArrayBuffer(str.length);
var bufView = new Uint8Array(buf);
for (var i=0, strLen=str.length; i<strLen; i++) {
bufView[i] = str.charCodeAt(i);
}
return buf;
}
var fileCollection = new FS.Collection('files', {
stores: [
new FS.Store.GridFS('files')
],
uploader: null
});
// Set up server stuff
if (Meteor.isServer) {
var fs = Npm.require('fs');
var temp = Npm.require('temp');
var path = Npm.require('path');
// Automatically track and cleanup files at exit
temp.track();
// Set up HTTP method URL used by client tests
var conf = {
get: function () {
var buf = new Buffer('Hello World');
this.setContentType('text/plain');
return buf;
},
head: function () {
var buf = new Buffer('Hello World');
this.setContentType('text/plain');
this.addHeader('Content-Length', buf.length);
buf = null;
}
};
HTTP.methods({
'test': conf,
'test.txt': conf
});
// Save temp file for testing with
function openTempFile(name, callback) {
return temp.open(name, callback);
}
var openTempFileSync = Meteor.wrapAsync(openTempFile);
var info = openTempFileSync({suffix: '.txt'});
var tempFilePath = info.path;
var tempFileName = path.basename(tempFilePath);
fs.writeSync(info.fd, 'Hello World');
fs.closeSync(info.fd);
fileCollection.allow({
insert: function () {
return true;
},
update: function () {
return true;
},
remove: function () {
return true;
}
});
Meteor.publish("files", function () {
return fileCollection.find();
});
}
if (Meteor.isClient) {
Meteor.subscribe("files");
}
Tinytest.add('cfs-file - test environment', function(test) {
test.isTrue(typeof FS.Collection !== 'undefined', 'test environment not initialized FS.Collection');
test.isTrue(typeof FS.File !== 'undefined', 'test environment not initialized FS.File');
});
Tinytest.add('cfs-file - construction', function(test) {
// Normal object provided will extend the fileObj
var f = new FS.File({foo: "bar"});
test.equal(f.foo, "bar");
// If passed another FS.File instance, we clone it
var f2 = new FS.File(f);
test.equal(f2.foo, "bar");
});
// Types of data and how we support attaching:
//
// Type C/S Constructor attachData w/ callback attachData w/o callback
// ----------------------------------------------------------------------------------------------
// Buffer Server No Yes Yes
// ArrayBuffer Both No Yes Yes
// Binary Both No Yes Yes
// Data URI Both Yes Yes Yes
// URL Both Yes on Server Yes Yes on Server
// Filepath Server Yes Yes Yes
// File Client Yes Yes Yes
// Blob Client Yes Yes Yes
function doAttachDataConstructorTest(data, name, test) {
var f = new FS.File(data);
if (!name) {
test.isUndefined(f.name());
} else {
test.equal(f.name(), name);
}
test.equal(f.type(), "text/plain");
test.equal(f.size(), 11);
}
function doAttachDataSyncTest(data, opts, name, test) {
var f = new FS.File();
f.attachData(data, opts);
if (!name) {
test.isUndefined(f.name());
} else {
test.equal(f.name(), name);
}
test.equal(f.type(), "text/plain");
test.equal(f.size(), 11);
}
function doAttachDataAsyncTest(data, opts, name, test, next) {
var f = new FS.File();
f.attachData(data, opts, function (error) {
test.isFalse(!!error);
if (!name) {
test.isUndefined(f.name());
} else {
test.equal(f.name(), name);
}
test.equal(f.type(), "text/plain");
test.equal(f.size(), 11);
next();
});
}
/*
* BUFFER
*/
if (Meteor.isServer) {
Tinytest.add('cfs-file - attachData sync - Buffer', function(test) {
doAttachDataSyncTest(new Buffer('Hello World'), {type: "text/plain"}, null, test);
});
Tinytest.addAsync('cfs-file - attachData async - Buffer', function(test, next) {
doAttachDataAsyncTest(new Buffer('Hello World'), {type: "text/plain"}, null, test, next);
});
}
/*
* ARRAYBUFFER
*/
Tinytest.add('cfs-file - attachData sync - ArrayBuffer', function(test) {
doAttachDataSyncTest(str2ab('Hello World'), {type: "text/plain"}, null, test);
});
Tinytest.addAsync('cfs-file - attachData async - ArrayBuffer', function(test, next) {
doAttachDataAsyncTest(str2ab('Hello World'), {type: "text/plain"}, null, test, next);
});
/*
* Binary
*/
Tinytest.add('cfs-file - attachData sync - Binary', function(test) {
doAttachDataSyncTest(new Uint8Array(str2ab('Hello World')), {type: "text/plain"}, null, test);
});
Tinytest.addAsync('cfs-file - attachData async - Binary', function(test, next) {
doAttachDataAsyncTest(new Uint8Array(str2ab('Hello World')), {type: "text/plain"}, null, test, next);
});
/*
* Data URI
*/
var dataUri = 'data:text/plain;base64,SGVsbG8gV29ybGQ='; //'Hello World'
Tinytest.add('cfs-file - attachData sync - Data URI', function(test) {
doAttachDataSyncTest(dataUri, null, null, test);
});
Tinytest.addAsync('cfs-file - attachData async - Data URI', function(test, next) {
doAttachDataAsyncTest(dataUri, null, null, test, next);
});
Tinytest.add('cfs-file - attachData from constructor - Data URI', function(test) {
doAttachDataConstructorTest(dataUri, null, test);
});
/*
* URL
*/
var url = Meteor.absoluteUrl('test');
var url2 = Meteor.absoluteUrl('test.txt');
if (Meteor.isServer) {
Tinytest.add('cfs-file - attachData sync - URL', function(test) {
doAttachDataSyncTest(url, null, null, test);
doAttachDataSyncTest(url2, null, 'test.txt', test);
});
Tinytest.add('cfs-file - attachData from constructor - URL', function(test) {
doAttachDataConstructorTest(url, null, test);
doAttachDataConstructorTest(url2, 'test.txt', test);
});
}
Tinytest.addAsync('cfs-file - attachData async - URL', function(test, next) {
doAttachDataAsyncTest(url, null, null, test, function () {
doAttachDataAsyncTest(url2, null, 'test.txt', test, next);
});
});
/*
* Filepath
*/
if (Meteor.isServer) {
Tinytest.add('cfs-file - attachData sync - Filepath', function(test) {
doAttachDataSyncTest(tempFilePath, null, tempFileName, test);
});
Tinytest.addAsync('cfs-file - attachData async - Filepath', function(test, next) {
doAttachDataAsyncTest(tempFilePath, null, tempFileName, test, next);
});
Tinytest.add('cfs-file - attachData from constructor - Filepath', function(test) {
doAttachDataConstructorTest(tempFilePath, tempFileName, test);
});
}
/*
* Blob
*/
if (Meteor.isClient) {
var blob = new Blob(['Hello World'], {type : 'text/plain'});
Tinytest.add('cfs-file - attachData sync - Blob', function(test) {
doAttachDataSyncTest(blob, null, null, test);
});
Tinytest.addAsync('cfs-file - attachData async - Blob', function(test, next) {
doAttachDataAsyncTest(blob, null, null, test, next);
});
Tinytest.add('cfs-file - attachData from constructor - Blob', function(test) {
doAttachDataConstructorTest(blob, null, test);
});
}
/*
* Collection Mounting
*/
Tinytest.add('cfs-file - isMounted', function(test) {
var f = new FS.File();
test.isFalse(!!f.isMounted());
f.collectionName = "files";
test.isTrue(!!f.isMounted());
});
/*
* name/extension
*/
Tinytest.add('cfs-file - name/extension', function(test) {
var f = new FS.File();
// Set names
f.name("foo.pdf");
f.name("bar.txt", {store: "files"});
// Get names
test.equal(f.name(), "foo.pdf");
test.equal(f.name({store: "files"}), "bar.txt");
// Get extensions
test.equal(f.extension(), "pdf");
test.equal(f.extension({store: "files"}), "txt");
// Now change extensions
f.extension("txt");
f.extension("pdf", {store: "files"});
// Get changed extensions
test.equal(f.extension(), "txt");
test.equal(f.extension({store: "files"}), "pdf");
});
/*
* size
*/
Tinytest.add('cfs-file - size', function(test) {
var f = new FS.File();
// Set size
f.size(1);
f.size(2, {store: "files"});
// Get size
test.equal(f.size(), 1);
test.equal(f.size({store: "files"}), 2);
});
/*
* type
*/
Tinytest.add('cfs-file - type', function(test) {
var f = new FS.File();
// Set type
f.type("image/png");
f.type("image/jpg", {store: "files"});
// Get type
test.equal(f.type(), "image/png");
test.equal(f.type({store: "files"}), "image/jpg");
});
/*
* updatedAt
*/
Tinytest.add('cfs-file - updatedAt', function(test) {
var f = new FS.File();
var d1 = new Date("2014-01-01");
var d2 = new Date("2014-02-01");
// Set updatedAt
f.updatedAt(d1);
f.updatedAt(d2, {store: "files"});
// Get updatedAt
test.equal(f.updatedAt().getTime(), d1.getTime());
test.equal(f.updatedAt({store: "files"}).getTime(), d2.getTime());
});
/*
* update, uploadProgress, and isUploaded
*/
Tinytest.addAsync('cfs-file - update, uploadProgress, and isUploaded', function(test, next) {
// Progress is based on chunkCount/chunkSum
var f = new FS.File('data:text/plain;base64,SGVsbG8gV29ybGQ=');
fileCollection.insert(f, function () {
f.update({$set: {chunkSum: 2, chunkCount: 1}}, function (error, result) {
test.isFalse(!!error);
test.equal(f.uploadProgress(), 50);
test.isFalse(f.isUploaded());
// But if uploadedAt is set, we should always get 100
f.update({$set: {uploadedAt: new Date}}, function (error, result) {
test.isFalse(!!error);
test.equal(f.uploadProgress(), 100);
test.isTrue(f.isUploaded());
next();
});
});
});
});
/*
* remove
*/
Tinytest.addAsync('cfs-file - remove', function(test, next) {
var f = new FS.File('data:text/plain;base64,SGVsbG8gV29ybGQ=');
var newId;
fileCollection.insert(f, function (error, fileObj) {
test.isFalse(!!error);
test.instanceOf(fileObj, FS.File);
newId = fileObj._id;
test.isTrue(!!fileCollection.findOne(newId));
// Wait 5 seconds to remove; otherwise we could
// cause errors with the tempstore or SA trying
// to save.
Meteor.setTimeout(function () {
fileObj.remove(function (error, result) {
test.isFalse(!!error);
test.equal(result, 1);
test.isFalse(!!fileCollection.findOne(newId));
next();
});
}, 5000);
});
});
if (Meteor.isServer) {
/*
* createWriteStream
*/
Tinytest.add('cfs-file - createWriteStream', function(test) {
//TODO
test.isTrue(true);
});
/*
* createReadStream
*/
Tinytest.add('cfs-file - createReadStream', function(test) {
//TODO
test.isTrue(true);
});
}
//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,5 @@
language: node_js
node_js:
- "0.10"
before_install:
- "curl -L http://git.io/s0Zu-w | /bin/sh"

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,41 @@
wekan-cfs-filesystem
=========================
NOTE: This package is under active development right now (2014-3-31). It has
bugs and the API may continue to change. Please help test it and fix bugs,
but don't use in production yet.
A Meteor package that adds local server filesystem storage for
[CollectionFS](https://github.com/zcfs/Meteor-CollectionFS). When you
use this storage adapter, file data is stored in a directory of your choosing
on the same server on which your Meteor app is running.
## Installation
Install using Meteorite. When in a Meteor app directory, enter:
```
$ meteor add wekan-cfs-filesystem
```
## Important Note
Note that using this Storage Adapter on the free Meteor deployment servers on `*.meteor.com` will cause a reset of files at every code deploy. You may want to have a look at the [GridFS Storage Adapter](https://github.com/zcfs/Meteor-CollectionFS/tree/devel/packages/gridfs) for persistent file storage.
## Usage
```js
var imageStore = new FS.Store.FileSystem("images", {
path: "~/app-files/images", //optional, default is "/cfs/files" path within app container
transformWrite: myTransformWriteFunction, //optional
transformRead: myTransformReadFunction, //optional
maxTries: 1 //optional, default 5
});
Images = new FS.Collection("images", {
stores: [imageStore]
});
```
Refer to the [CollectionFS](https://github.com/zcfs/Meteor-CollectionFS)
package documentation for more information.

View file

@ -0,0 +1,10 @@
// On the client we have just a shell
FS.Store.FileSystem = function(name, options) {
var self = this;
if (!(self instanceof FS.Store.FileSystem))
throw new Error('FS.Store.FileSystem missing keyword "new"');
return new FS.StorageAdapter(name, options, {
typeName: 'storage.filesystem'
});
};

View file

@ -0,0 +1,157 @@
var fs = Npm.require('fs');
var path = Npm.require('path');
var mkdirp = Npm.require('mkdirp');
//var chokidar = Npm.require('chokidar');
FS.Store.FileSystem = function(name, options) {
var self = this;
if (!(self instanceof FS.Store.FileSystem))
throw new Error('FS.Store.FileSystem missing keyword "new"');
// We allow options to be string/path empty or options.path
options = (options !== ''+options) ? options || {} : { path: options };
// Provide a default FS directory one level up from the build/bundle directory
var pathname = options.path;
if (!pathname && __meteor_bootstrap__ && __meteor_bootstrap__.serverDir) {
pathname = path.join(__meteor_bootstrap__.serverDir, '../../../cfs/files/' + name);
}
if (!pathname)
throw new Error('FS.Store.FileSystem unable to determine path');
// Check if we have '~/foo/bar'
if (pathname.split(path.sep)[0] === '~') {
var homepath = process.env.HOME || process.env.HOMEPATH || process.env.USERPROFILE;
if (homepath) {
pathname = pathname.replace('~', homepath);
} else {
throw new Error('FS.Store.FileSystem unable to resolve "~" in path');
}
}
// Set absolute path
var absolutePath = path.resolve(pathname);
// Ensure the path exists
mkdirp.sync(absolutePath);
FS.debug && console.log(name + ' FileSystem mounted on: ' + absolutePath);
return new FS.StorageAdapter(name, options, {
typeName: 'storage.filesystem',
fileKey: function(fileObj) {
// Lookup the copy
var store = fileObj && fileObj._getInfo(name);
// If the store and key is found return the key
if (store && store.key) return store.key;
var filename = fileObj.name();
var filenameInStore = fileObj.name({store: name});
// If no store key found we resolve / generate a key
return fileObj.collectionName + '-' + fileObj._id + '-' + (filenameInStore || filename);
},
createReadStream: function(fileKey, options) {
// this is the Storage adapter scope
var filepath = path.join(absolutePath, fileKey);
// return the read stream - Options allow { start, end }
return fs.createReadStream(filepath, options);
},
createWriteStream: function(fileKey, options) {
options = options || {};
// this is the Storage adapter scope
var filepath = path.join(absolutePath, fileKey);
// Return the stream handle
var writeStream = fs.createWriteStream(filepath, options);
// The filesystem does not emit the "end" event only close - so we
// manually send the end event
writeStream.on('close', function() {
if (FS.debug) console.log('SA FileSystem - DONE!! fileKey: "' + fileKey + '"');
// Get the exact size of the stored file, so that we can pass it to onEnd/onStored.
// Since stream transforms might have altered the size, this is the best way to
// ensure we update the fileObj.copies with the correct size.
try {
// Get the stats of the file
var stats = fs.statSync(filepath);
// Emit end and return the fileKey, size, and updated date
writeStream.emit('stored', {
fileKey: fileKey,
size: stats.size,
storedAt: stats.mtime
});
} catch(err) {
// On error we emit the error on
writeStream.emit('error', err);
}
});
return writeStream;
},
remove: function(fileKey, callback) {
// this is the Storage adapter scope
var filepath = path.join(absolutePath, fileKey);
// Call node unlink file
fs.unlink(filepath, function (error, result) {
if (error && error.errno === 34) {
console.warn("SA FileSystem: Could not delete " + filepath + " because the file was not found.");
callback && callback(null);
} else {
callback && callback(error, result);
}
});
},
stats: function(fileKey, callback) {
// this is the Storage adapter scope
var filepath = path.join(absolutePath, fileKey);
if (typeof callback === 'function') {
fs.stat(filepath, callback);
} else {
return fs.statSync(filepath);
}
}
// Add this back and add the chokidar dependency back when we make this work eventually
// watch: function(callback) {
// function fileKey(filePath) {
// return filePath.replace(absolutePath, "");
// }
// FS.debug && console.log('Watching ' + absolutePath);
// // chokidar seems to be most widely used and production ready watcher
// var watcher = chokidar.watch(absolutePath, {ignored: /\/\./, ignoreInitial: true});
// watcher.on('add', Meteor.bindEnvironment(function(filePath, stats) {
// callback("change", fileKey(filePath), {
// name: path.basename(filePath),
// type: null,
// size: stats.size,
// utime: stats.mtime
// });
// }, function(err) {
// throw err;
// }));
// watcher.on('change', Meteor.bindEnvironment(function(filePath, stats) {
// callback("change", fileKey(filePath), {
// name: path.basename(filePath),
// type: null,
// size: stats.size,
// utime: stats.mtime
// });
// }, function(err) {
// throw err;
// }));
// watcher.on('unlink', Meteor.bindEnvironment(function(filePath) {
// callback("remove", fileKey(filePath));
// }, function(err) {
// throw err;
// }));
// }
});
};

View file

@ -0,0 +1,24 @@
Package.describe({
git: 'https://github.com/zcfs/Meteor-cfs-filesystem.git',
name: 'wekan-cfs-filesystem',
version: '0.1.2',
summary: "Filesystem storage adapter for CollectionFS"
});
Npm.depends({
//chokidar: "0.8.2",
mkdirp: "0.3.5"
});
Package.onUse(function(api) {
api.versionsFrom('1.0');
api.use(['wekan-cfs-base-package@0.0.30', 'wekan-cfs-storage-adapter@0.2.1']);
api.addFiles('filesystem.server.js', 'server');
api.addFiles('filesystem.client.js', 'client');
});
Package.onTest(function(api) {
api.use(['wekan-cfs-filesystem', 'test-helpers', 'tinytest'], 'server');
api.addFiles('tests.js', 'server');
});

View file

@ -0,0 +1,9 @@
//TODO
/*
* FileSystem Tests (Server Only)
*
* Create FS SA and use for all tests (verify the "~" correctly uses the home directory)
* Test get, getBytes, put, remove, stats. Verify that the correct things happen
* in the correct places on the local filesystem.
*/

View file

@ -0,0 +1,44 @@
function equals(a, b) {
return !!(EJSON.stringify(a) === EJSON.stringify(b));
}
Tinytest.add('cfs-filesystem - client - test environment', function(test) {
test.isTrue(typeof FS.Collection !== 'undefined', 'test environment not initialized FS.Collection');
test.isTrue(typeof CFSErrorType !== 'undefined', 'test environment not initialized CFSErrorType');
});
/*
* FS.File Client Tests
*
* construct FS.File with no arguments
* construct FS.File passing in File
* construct FS.File passing in Blob
* load blob into FS.File and then call FS.File.toDataUrl
* call FS.File.setDataFromBinary, then FS.File.getBlob(); make sure correct data is returned
* load blob into FS.File and then call FS.File.getBinary() with and without start/end; make sure correct data is returned
* construct FS.File, set FS.File.collectionName to a CFS name, and then test FS.File.update/remove/get/put/del/url
* set FS.File.name to a filename and test that FS.File.getExtension() returns the extension
* load blob into FS.File and make sure FS.File.saveLocal initiates a download (possibly can't do automatically)
*
*/
//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,49 @@
function equals(a, b) {
return !!(EJSON.stringify(a) === EJSON.stringify(b));
}
Tinytest.add('cfs-filesystem - server - test environment', function(test) {
test.isTrue(typeof FS.Collection !== 'undefined', 'test environment not initialized FS.Collection');
test.isTrue(typeof CFSErrorType !== 'undefined', 'test environment not initialized CFSErrorType');
});
/*
* FS.File Server Tests
*
* construct FS.File with no arguments
* load data with FS.File.setDataFromBuffer
* load data with FS.File.setDataFromBinary
* load data and then call FS.File.toDataUrl with and without callback
* load buffer into FS.File and then call FS.File.getBinary with and without start/end; make sure correct data is returned
* construct FS.File, set FS.File.collectionName to a CFS name, and then test FS.File.update/remove/get/put/del/url
* (call these with and without callback to test sync vs. async)
* set FS.File.name to a filename and test that FS.File.getExtension() returns the extension
*
*
* FS.Collection Server Tests
*
* Make sure options.filter is respected
*
*
*/
//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,5 @@
language: node_js
node_js:
- "0.10"
before_install:
- "curl -L http://git.io/s0Zu-w | /bin/sh"

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,48 @@
wekan-cfs-gridfs
=========================
NOTE: This package is under active development right now (2014-3-31). It has
bugs and the API may continue to change. Please help test it and fix bugs,
but don't use in production yet.
A Meteor package that adds [GridFS](http://docs.mongodb.org/manual/core/gridfs/) file storage for
[CollectionFS](https://github.com/zcfs/Meteor-CollectionFS). When you
use this storage adapter, file data is stored in chunks in your MongoDB database.
## Installation
Install using Meteorite. When in a Meteor app directory, enter:
```
$ meteor add wekan-cfs-gridfs
```
## Usage
```js
var imageStore = new FS.Store.GridFS("images", {
mongoUrl: 'mongodb://127.0.0.1:27017/test/', // optional, defaults to Meteor's local MongoDB
mongoOptions: {...}, // optional, see note below
transformWrite: myTransformWriteFunction, //optional
transformRead: myTransformReadFunction, //optional
maxTries: 1, // optional, default 5
chunkSize: 1024*1024 // optional, default GridFS chunk size in bytes (can be overridden per file).
// Default: 2MB. Reasonable range: 512KB - 4MB
});
Images = new FS.Collection("images", {
stores: [imageStore]
});
```
More control over the MongoDB connection is available by specifying [MongoClient.connect options](http://mongodb.github.io/node-mongodb-native/driver-articles/mongoclient.html#mongoclient-connect-options) as a `mongoOptions` attribute in the options object on the constructor.
Refer to the [CollectionFS](https://github.com/zcfs/Meteor-CollectionFS)
package documentation for more information.
## API
[For Users](https://github.com/zcfs/Meteor-CollectionFS/blob/master/packages/gridfs/api.md)
[For Contributors](https://github.com/zcfs/Meteor-CollectionFS/blob/master/packages/gridfs/internal.api.md)

View file

@ -0,0 +1,69 @@
## cfs-gridfs Public API ##
GridFS storage adapter for CollectionFS
_API documentation automatically generated by [docmeteor](https://github.com/raix/docmeteor)._
-
### <a name="FS.Store.GridFS"></a>new *fsStore*.GridFS(name, options)&nbsp;&nbsp;<sub><i>Server</i></sub> ###
*This method __GridFS__ is defined in `FS.Store`*
__Arguments__
* __name__ *{String}*
The store name
* __options__ *{Object}*
* __beforeSave__ *{Function}* (Optional)
Function to run before saving a file from the server. The context of the function will be the `FS.File` instance we're saving. The function may alter its properties.
* __maxTries__ *{Number}* (Optional, Default = 5)
Max times to attempt saving a file
__Returns__ *{FS.StorageAdapter}*
An instance of FS.StorageAdapter.
Creates a GridFS store instance on the server. Inherits from FS.StorageAdapter
type.
> ```FS.Store.GridFS = function(name, options) { ...``` [gridfs.server.js:16](gridfs.server.js#L16)
-
### <a name="FS.Store.GridFS"></a>new *fsStore*.GridFS(name, options)&nbsp;&nbsp;<sub><i>Client</i></sub> ###
*This method __GridFS__ is defined in `FS.Store`*
__Arguments__
* __name__ *{String}*
The store name
* __options__ *{Object}*
* __beforeSave__ *{Function}* (Optional)
Function to run before saving a file from the client. The context of the function will be the `FS.File` instance we're saving. The function may alter its properties.
* __maxTries__ *{Number}* (Optional, Default = 5)
Max times to attempt saving a file
__Returns__ *{undefined}*
Creates a GridFS store instance on the client, which is just a shell object
storing some info.
> ```FS.Store.GridFS = function(name, options) { ...``` [gridfs.client.js:13](gridfs.client.js#L13)

View file

@ -0,0 +1,21 @@
/**
* @public
* @constructor
* @param {String} name - The store name
* @param {Object} options
* @param {Function} [options.beforeSave] - Function to run before saving a file from the client. The context of the function will be the `FS.File` instance we're saving. The function may alter its properties.
* @param {Number} [options.maxTries=5] - Max times to attempt saving a file
* @returns {undefined}
*
* Creates a GridFS store instance on the client, which is just a shell object
* storing some info.
*/
FS.Store.GridFS = function(name, options) {
var self = this;
if (!(self instanceof FS.Store.GridFS))
throw new Error('FS.Store.GridFS missing keyword "new"');
return new FS.StorageAdapter(name, options, {
typeName: 'storage.gridfs'
});
};

View file

@ -0,0 +1,176 @@
var path = Npm.require('path');
var mongodb = Npm.require('mongodb');
var ObjectID = Npm.require('mongodb').ObjectID;
var Grid = Npm.require('gridfs-stream');
//var Grid = Npm.require('gridfs-locking-stream');
var chunkSize = 1024*1024*2; // 256k is default GridFS chunk size, but performs terribly for largish files
/**
* @public
* @constructor
* @param {String} name - The store name
* @param {Object} options
* @param {Function} [options.beforeSave] - Function to run before saving a file from the server. The context of the function will be the `FS.File` instance we're saving. The function may alter its properties.
* @param {Number} [options.maxTries=5] - Max times to attempt saving a file
* @returns {FS.StorageAdapter} An instance of FS.StorageAdapter.
*
* Creates a GridFS store instance on the server. Inherits from FS.StorageAdapter
* type.
*/
FS.Store.GridFS = function(name, options) {
var self = this;
options = options || {};
var gridfsName = name;
var mongoOptions = options.mongoOptions || {};
if (!(self instanceof FS.Store.GridFS))
throw new Error('FS.Store.GridFS missing keyword "new"');
if (!options.mongoUrl) {
options.mongoUrl = process.env.MONGO_URL;
// When using a Meteor MongoDB instance, preface name with "cfs_gridfs."
gridfsName = "cfs_gridfs." + name;
}
if (!options.mongoOptions) {
options.mongoOptions = { db: { native_parser: true }, server: { auto_reconnect: true }};
}
if (options.chunkSize) {
chunkSize = options.chunkSize;
}
return new FS.StorageAdapter(name, options, {
typeName: 'storage.gridfs',
fileKey: function(fileObj) {
// We should not have to mount the file here - We assume its taken
// care of - Otherwise we create new files instead of overwriting
var key = {
_id: null,
filename: null
};
// If we're passed a fileObj, we retrieve the _id and filename from it.
if (fileObj) {
var info = fileObj._getInfo(name, {updateFileRecordFirst: false});
key._id = info.key || null;
key.filename = info.name || fileObj.name({updateFileRecordFirst: false}) || (fileObj.collectionName + '-' + fileObj._id);
}
// If key._id is null at this point, createWriteStream will let GridFS generate a new ID
return key;
},
createReadStream: function(fileKey, options) {
options = options || {};
// Init GridFS
var gfs = new Grid(self.db, mongodb);
// Set the default streamning settings
var settings = {
_id: new ObjectID(fileKey._id),
root: gridfsName
};
// Check if this should be a partial read
if (typeof options.start !== 'undefined' && typeof options.end !== 'undefined' ) {
// Add partial info
settings.range = {
startPos: options.start,
endPos: options.end
};
}
FS.debug && console.log('GRIDFS', settings);
return gfs.createReadStream(settings);
},
createWriteStream: function(fileKey, options) {
options = options || {};
// Init GridFS
var gfs = new Grid(self.db, mongodb);
var opts = {
filename: fileKey.filename,
mode: 'w',
root: gridfsName,
chunk_size: options.chunk_size || chunkSize,
// We allow aliases, metadata and contentType to be passed in via
// options
aliases: options.aliases || [],
metadata: options.metadata || null,
content_type: options.contentType || 'application/octet-stream'
};
if (fileKey._id) {
opts._id = new ObjectID(fileKey._id);
}
var writeStream = gfs.createWriteStream(opts);
writeStream.on('close', function(file) {
if (!file) {
// gridfs-stream will emit "close" without passing a file
// if there is an error. We can simply exit here because
// the "error" listener will also be called in this case.
return;
}
if (FS.debug) console.log('SA GridFS - DONE!');
// Emit end and return the fileKey, size, and updated date
writeStream.emit('stored', {
// Set the generated _id so that we know it for future reads and writes.
// We store the _id as a string and only convert to ObjectID right before
// reading, writing, or deleting. If we store the ObjectID itself,
// Meteor (EJSON?) seems to convert it to a LocalCollection.ObjectID,
// which GFS doesn't understand.
fileKey: file._id.toString(),
size: file.length,
storedAt: file.uploadDate || new Date()
});
});
writeStream.on('error', function(error) {
console.log('SA GridFS - ERROR!', error);
});
return writeStream;
},
remove: function(fileKey, callback) {
// Init GridFS
var gfs = new Grid(self.db, mongodb);
try {
gfs.remove({ _id: new ObjectID(fileKey._id), root: gridfsName }, callback);
} catch(err) {
callback(err);
}
},
// Not implemented
watch: function() {
throw new Error("GridFS storage adapter does not support the sync option");
},
init: function(callback) {
mongodb.MongoClient.connect(options.mongoUrl, mongoOptions, function (err, db) {
if (err) { return callback(err); }
self.db = db;
// ensure that indexes are added as otherwise CollectionFS fails for Mongo >= 3.0
var collection = new Mongo.Collection(gridfsName);
collection.rawCollection().ensureIndex({ "files_id": 1, "n": 1});
callback(null);
});
}
});
};

View file

@ -0,0 +1,75 @@
## Public and Private API ##
_API documentation automatically generated by [docmeteor](https://github.com/raix/docmeteor)._
***
__File: ["gridfs.server.js"](gridfs.server.js) Where: {server}__
***
### <a name="FS.Store.GridFS"></a>new *fsStore*.GridFS(name, options)&nbsp;&nbsp;<sub><i>Server</i></sub> ###
*This method __GridFS__ is defined in `FS.Store`*
__Arguments__
* __name__ *{String}*
The store name
* __options__ *{Object}*
* __beforeSave__ *{Function}* (Optional)
Function to run before saving a file from the server. The context of the function will be the `FS.File` instance we're saving. The function may alter its properties.
* __maxTries__ *{Number}* (Optional, Default = 5)
Max times to attempt saving a file
__Returns__ *{FS.StorageAdapter}*
An instance of FS.StorageAdapter.
Creates a GridFS store instance on the server. Inherits from FS.StorageAdapter
type.
> ```FS.Store.GridFS = function(name, options) { ...``` [gridfs.server.js:16](gridfs.server.js#L16)
***
__File: ["gridfs.client.js"](gridfs.client.js) Where: {client}__
***
### <a name="FS.Store.GridFS"></a>new *fsStore*.GridFS(name, options)&nbsp;&nbsp;<sub><i>Client</i></sub> ###
*This method __GridFS__ is defined in `FS.Store`*
__Arguments__
* __name__ *{String}*
The store name
* __options__ *{Object}*
* __beforeSave__ *{Function}* (Optional)
Function to run before saving a file from the client. The context of the function will be the `FS.File` instance we're saving. The function may alter its properties.
* __maxTries__ *{Number}* (Optional, Default = 5)
Max times to attempt saving a file
__Returns__ *{undefined}*
Creates a GridFS store instance on the client, which is just a shell object
storing some info.
> ```FS.Store.GridFS = function(name, options) { ...``` [gridfs.client.js:13](gridfs.client.js#L13)

View file

@ -0,0 +1,24 @@
Package.describe({
name: 'wekan-cfs-gridfs',
version: '0.0.34',
summary: 'GridFS storage adapter for CollectionFS',
git: 'https://github.com/zcfs/Meteor-cfs-gridfs.git'
});
Npm.depends({
mongodb: '2.2.9',
'gridfs-stream': '1.1.1'
//'gridfs-locking-stream': '0.0.3'
});
Package.onUse(function (api) {
api.use(['wekan-cfs-base-package@0.0.30', 'wekan-cfs-storage-adapter@0.2.3', 'ecmascript@0.1.0']);
api.addFiles('gridfs.server.js', 'server');
api.addFiles('gridfs.client.js', 'client');
});
Package.onTest(function (api) {
api.use(['wekan-cfs-gridfs', 'test-helpers', 'tinytest'], 'server');
api.addFiles('tests/server-tests.js', 'server');
api.addFiles('tests/client-tests.js', 'client');
});

View file

@ -0,0 +1,44 @@
function equals(a, b) {
return !!(EJSON.stringify(a) === EJSON.stringify(b));
}
Tinytest.add('cfs-gridfs - client - test environment', function(test) {
test.isTrue(typeof FS.Collection !== 'undefined', 'test environment not initialized FS.Collection');
test.isTrue(typeof CFSErrorType !== 'undefined', 'test environment not initialized CFSErrorType');
});
/*
* FS.File Client Tests
*
* construct FS.File with no arguments
* construct FS.File passing in File
* construct FS.File passing in Blob
* load blob into FS.File and then call FS.File.toDataUrl
* call FS.File.setDataFromBinary, then FS.File.getBlob(); make sure correct data is returned
* load blob into FS.File and then call FS.File.getBinary() with and without start/end; make sure correct data is returned
* construct FS.File, set FS.File.collectionName to a CFS name, and then test FS.File.update/remove/get/put/del/url
* set FS.File.name to a filename and test that FS.File.getExtension() returns the extension
* load blob into FS.File and make sure FS.File.saveLocal initiates a download (possibly can't do automatically)
*
*/
//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,49 @@
function equals(a, b) {
return !!(EJSON.stringify(a) === EJSON.stringify(b));
}
Tinytest.add('cfs-gridfs - server - test environment', function(test) {
test.isTrue(typeof FS.Collection !== 'undefined', 'test environment not initialized FS.Collection');
test.isTrue(typeof CFSErrorType !== 'undefined', 'test environment not initialized CFSErrorType');
});
/*
* FS.File Server Tests
*
* construct FS.File with no arguments
* load data with FS.File.setDataFromBuffer
* load data with FS.File.setDataFromBinary
* load data and then call FS.File.toDataUrl with and without callback
* load buffer into FS.File and then call FS.File.getBinary with and without start/end; make sure correct data is returned
* construct FS.File, set FS.File.collectionName to a CFS name, and then test FS.File.update/remove/get/put/del/url
* (call these with and without callback to test sync vs. async)
* set FS.File.name to a filename and test that FS.File.getExtension() returns the extension
*
*
* FS.Collection Server Tests
*
* Make sure options.filter is respected
*
*
*/
//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,18 @@
# .editorconfig
# Meteor adapted EditorConfig, http://EditorConfig.org
# By RaiX 2013
root = true
[*.js]
end_of_line = lf
insert_final_newline = true
indent_style = space
indent_size = 2
trim_trailing_whitespace = true
charset = utf-8
max_line_length = 80
indent_brace_style = 1TBS
spaces_around_operators = true
quote_type = auto
# curly_bracket_next_line = true

Some files were not shown because too many files have changed in this diff Show more