From 72604d5d73237f1c6575c9877a56b593532d9701 Mon Sep 17 00:00:00 2001 From: Lauri Ojansivu Date: Sun, 14 Jul 2019 21:09:47 +0300 Subject: Add cfs-access-point as local package included in Wekan repo, to hopefully make Wekan all deps work directly with command `meteor`. Thanks to xet7 ! --- packages/wekan-cfs-access-point/isopack.json | 50 ++ packages/wekan-cfs-access-point/os-legacy.json | 67 ++ .../cfs_access-point/packages/cfs_access-point.js | 860 ++++++++++++++++++++ packages/wekan-cfs-access-point/os.json | 51 ++ .../os/packages/cfs_access-point.js | 880 +++++++++++++++++++++ packages/wekan-cfs-access-point/package.js | 8 + packages/wekan-cfs-access-point/unipackage.json | 25 + .../wekan-cfs-access-point/web.browser-legacy.json | 55 ++ .../cfs_access-point/packages/cfs_access-point.js | 262 ++++++ packages/wekan-cfs-access-point/web.browser.json | 51 ++ .../web.browser/packages/cfs_access-point.js | 262 ++++++ .../wekan-cfs-access-point/web.cordova-legacy.json | 55 ++ .../cfs_access-point/packages/cfs_access-point.js | 262 ++++++ packages/wekan-cfs-access-point/web.cordova.json | 51 ++ .../web.cordova/packages/cfs_access-point.js | 262 ++++++ 15 files changed, 3201 insertions(+) create mode 100644 packages/wekan-cfs-access-point/isopack.json create mode 100644 packages/wekan-cfs-access-point/os-legacy.json create mode 100644 packages/wekan-cfs-access-point/os-legacy/packages/cfs_access-point/packages/cfs_access-point.js create mode 100644 packages/wekan-cfs-access-point/os.json create mode 100644 packages/wekan-cfs-access-point/os/packages/cfs_access-point.js create mode 100644 packages/wekan-cfs-access-point/package.js create mode 100644 packages/wekan-cfs-access-point/unipackage.json create mode 100644 packages/wekan-cfs-access-point/web.browser-legacy.json create mode 100644 packages/wekan-cfs-access-point/web.browser-legacy/packages/cfs_access-point/packages/cfs_access-point.js create mode 100644 packages/wekan-cfs-access-point/web.browser.json create mode 100644 packages/wekan-cfs-access-point/web.browser/packages/cfs_access-point.js create mode 100644 packages/wekan-cfs-access-point/web.cordova-legacy.json create mode 100644 packages/wekan-cfs-access-point/web.cordova-legacy/packages/cfs_access-point/packages/cfs_access-point.js create mode 100644 packages/wekan-cfs-access-point/web.cordova.json create mode 100644 packages/wekan-cfs-access-point/web.cordova/packages/cfs_access-point.js (limited to 'packages') diff --git a/packages/wekan-cfs-access-point/isopack.json b/packages/wekan-cfs-access-point/isopack.json new file mode 100644 index 00000000..55891efb --- /dev/null +++ b/packages/wekan-cfs-access-point/isopack.json @@ -0,0 +1,50 @@ +{ + "isopack-2": { + "name": "cfs:access-point", + "summary": "CollectionFS, add ddp and http accesspoint capability", + "version": "0.1.49", + "isTest": false, + "builds": [ + { + "kind": "main", + "arch": "os", + "path": "os.json" + }, + { + "kind": "main", + "arch": "web.browser", + "path": "web.browser.json" + }, + { + "kind": "main", + "arch": "web.cordova", + "path": "web.cordova.json" + } + ], + "plugins": [] + }, + "isopack-1": { + "name": "cfs:access-point", + "summary": "CollectionFS, add ddp and http accesspoint capability", + "version": "0.1.49", + "isTest": false, + "builds": [ + { + "kind": "main", + "arch": "os", + "path": "os-legacy.json" + }, + { + "kind": "main", + "arch": "web.browser", + "path": "web.browser-legacy.json" + }, + { + "kind": "main", + "arch": "web.cordova", + "path": "web.cordova-legacy.json" + } + ], + "plugins": [] + } +} \ No newline at end of file diff --git a/packages/wekan-cfs-access-point/os-legacy.json b/packages/wekan-cfs-access-point/os-legacy.json new file mode 100644 index 00000000..405ae7cf --- /dev/null +++ b/packages/wekan-cfs-access-point/os-legacy.json @@ -0,0 +1,67 @@ +{ + "format": "unipackage-unibuild-pre1", + "uses": [ + { + "package": "meteor" + }, + { + "package": "cfs:base-package", + "constraint": "0.0.30" + }, + { + "package": "cfs:file", + "constraint": "0.1.16" + }, + { + "package": "check", + "constraint": "1.0.2" + }, + { + "package": "ejson", + "constraint": "1.0.4" + }, + { + "package": "cfs:http-methods", + "constraint": "0.0.29" + }, + { + "package": "cfs:http-publish", + "constraint": "0.0.13" + } + ], + "implies": [ + { + "package": "cfs:base-package", + "constraint": "" + } + ], + "resources": [ + { + "type": "prelink", + "file": "os-legacy/packages/cfs_access-point/packages/cfs_access-point.js", + "length": 104737, + "offset": 0, + "servePath": "/packages/cfs_access-point/packages/cfs_access-point.js" + } + ], + "packageVariables": [ + { + "name": "rootUrlPathPrefix" + }, + { + "name": "baseUrl" + }, + { + "name": "getHeaders" + }, + { + "name": "getHeadersByCollection" + }, + { + "name": "_existingMountPoints" + }, + { + "name": "mountUrls" + } + ] +} \ No newline at end of file diff --git a/packages/wekan-cfs-access-point/os-legacy/packages/cfs_access-point/packages/cfs_access-point.js b/packages/wekan-cfs-access-point/os-legacy/packages/cfs_access-point/packages/cfs_access-point.js new file mode 100644 index 00000000..e180a69c --- /dev/null +++ b/packages/wekan-cfs-access-point/os-legacy/packages/cfs_access-point/packages/cfs_access-point.js @@ -0,0 +1,860 @@ +(function () { + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// // +// packages/cfs:access-point/access-point-common.js // +// // +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // +rootUrlPathPrefix = __meteor_runtime_config__.ROOT_URL_PATH_PREFIX || ""; // 1 +// Adjust the rootUrlPathPrefix if necessary // 2 +if (rootUrlPathPrefix.length > 0) { // 3 + if (rootUrlPathPrefix.slice(0, 1) !== '/') { // 4 + rootUrlPathPrefix = '/' + rootUrlPathPrefix; // 5 + } // 6 + if (rootUrlPathPrefix.slice(-1) === '/') { // 7 + rootUrlPathPrefix = rootUrlPathPrefix.slice(0, -1); // 8 + } // 9 +} // 10 + // 11 +// prepend ROOT_URL when isCordova // 12 +if (Meteor.isCordova) { // 13 + rootUrlPathPrefix = Meteor.absoluteUrl(rootUrlPathPrefix.replace(/^\/+/, '')).replace(/\/+$/, ''); // 14 +} // 15 + // 16 +baseUrl = '/cfs'; // 17 +FS.HTTP = FS.HTTP || {}; // 18 + // 19 +// Note the upload URL so that client uploader packages know what it is // 20 +FS.HTTP.uploadUrl = rootUrlPathPrefix + baseUrl + '/files'; // 21 + // 22 +/** // 23 + * @method FS.HTTP.setBaseUrl // 24 + * @public // 25 + * @param {String} newBaseUrl - Change the base URL for the HTTP GET and DELETE endpoints. // 26 + * @returns {undefined} // 27 + */ // 28 +FS.HTTP.setBaseUrl = function setBaseUrl(newBaseUrl) { // 29 + // 30 + // Adjust the baseUrl if necessary // 31 + if (newBaseUrl.slice(0, 1) !== '/') { // 32 + newBaseUrl = '/' + newBaseUrl; // 33 + } // 34 + if (newBaseUrl.slice(-1) === '/') { // 35 + newBaseUrl = newBaseUrl.slice(0, -1); // 36 + } // 37 + // 38 + // Update the base URL // 39 + baseUrl = newBaseUrl; // 40 + // 41 + // Change the upload URL so that client uploader packages know what it is // 42 + FS.HTTP.uploadUrl = rootUrlPathPrefix + baseUrl + '/files'; // 43 + // 44 + // Remount URLs with the new baseUrl, unmounting the old, on the server only. // 45 + // If existingMountPoints is empty, then we haven't run the server startup // 46 + // code yet, so this new URL will be used at that point for the initial mount. // 47 + if (Meteor.isServer && !FS.Utility.isEmpty(_existingMountPoints)) { // 48 + mountUrls(); // 49 + } // 50 +}; // 51 + // 52 +/* // 53 + * FS.File extensions // 54 + */ // 55 + // 56 +/** // 57 + * @method FS.File.prototype.url Construct the file url // 58 + * @public // 59 + * @param {Object} [options] // 60 + * @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. // 66 + * @param {String} [options.storing=null] A URL to return while the file is being stored. // 67 + * @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. + * // 69 + * Returns the HTTP URL for getting the file or its metadata. // 70 + */ // 71 +FS.File.prototype.url = function(options) { // 72 + var self = this; // 73 + options = options || {}; // 74 + options = FS.Utility.extend({ // 75 + store: null, // 76 + auth: null, // 77 + download: false, // 78 + metadata: false, // 79 + brokenIsFine: false, // 80 + uploading: null, // return this URL while uploading // 81 + storing: null, // return this URL while storing // 82 + filename: null // override the filename that is shown to the user // 83 + }, options.hash || options); // check for "hash" prop if called as helper // 84 + // 85 + // Primarily useful for displaying a temporary image while uploading an image // 86 + if (options.uploading && !self.isUploaded()) { // 87 + return options.uploading; // 88 + } // 89 + // 90 + if (self.isMounted()) { // 91 + // See if we've stored in the requested store yet // 92 + var storeName = options.store || self.collection.primaryStore.name; // 93 + if (!self.hasStored(storeName)) { // 94 + if (options.storing) { // 95 + return options.storing; // 96 + } else if (!options.brokenIsFine) { // 97 + // We want to return null if we know the URL will be a broken // 98 + // link because then we can avoid rendering broken links, broken // 99 + // images, etc. // 100 + return null; // 101 + } // 102 + } // 103 + // 104 + // Add filename to end of URL if we can determine one // 105 + var filename = options.filename || self.name({store: storeName}); // 106 + if (typeof filename === "string" && filename.length) { // 107 + filename = '/' + filename; // 108 + } else { // 109 + filename = ''; // 110 + } // 111 + // 112 + // TODO: Could we somehow figure out if the collection requires login? // 113 + var authToken = ''; // 114 + if (Meteor.isClient && typeof Accounts !== "undefined" && typeof Accounts._storedLoginToken === "function") { // 115 + if (options.auth !== false) { // 116 + // Add reactive deps on the user // 117 + Meteor.userId(); // 118 + // 119 + var authObject = { // 120 + authToken: Accounts._storedLoginToken() || '' // 121 + }; // 122 + // 123 + // If it's a number, we use that as the expiration time (in seconds) // 124 + if (options.auth === +options.auth) { // 125 + authObject.expiration = FS.HTTP.now() + options.auth * 1000; // 126 + } // 127 + // 128 + // Set the authToken // 129 + var authString = JSON.stringify(authObject); // 130 + authToken = FS.Utility.btoa(authString); // 131 + } // 132 + } else if (typeof options.auth === "string") { // 133 + // If the user supplies auth token the user will be responsible for // 134 + // updating // 135 + authToken = options.auth; // 136 + } // 137 + // 138 + // Construct query string // 139 + var params = {}; // 140 + if (authToken !== '') { // 141 + params.token = authToken; // 142 + } // 143 + if (options.download) { // 144 + params.download = true; // 145 + } // 146 + if (options.store) { // 147 + // We use options.store here instead of storeName because we want to omit the queryString // 148 + // whenever possible, allowing users to have "clean" URLs if they want. The server will // 149 + // assume the first store defined on the server, which means that we are assuming that // 150 + // the first on the client is also the first on the server. If that's not the case, the // 151 + // store option should be supplied. // 152 + params.store = options.store; // 153 + } // 154 + var queryString = FS.Utility.encodeParams(params); // 155 + if (queryString.length) { // 156 + queryString = '?' + queryString; // 157 + } // 158 + // 159 + // Determine which URL to use // 160 + var area; // 161 + if (options.metadata) { // 162 + area = '/record'; // 163 + } else { // 164 + area = '/files'; // 165 + } // 166 + // 167 + // Construct and return the http method url // 168 + return rootUrlPathPrefix + baseUrl + area + '/' + self.collection.name + '/' + self._id + filename + queryString; // 169 + } // 170 + // 171 +}; // 172 + // 173 + // 174 + // 175 +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +}).call(this); + + + + + + +(function () { + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// // +// packages/cfs:access-point/access-point-handlers.js // +// // +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // +getHeaders = []; // 1 +getHeadersByCollection = {}; // 2 + // 3 +FS.HTTP.Handlers = {}; // 4 + // 5 +/** // 6 + * @method FS.HTTP.Handlers.Del // 7 + * @public // 8 + * @returns {any} response // 9 + * // 10 + * HTTP DEL request handler // 11 + */ // 12 +FS.HTTP.Handlers.Del = function httpDelHandler(ref) { // 13 + var self = this; // 14 + var opts = FS.Utility.extend({}, self.query || {}, self.params || {}); // 15 + // 16 + // If DELETE request, validate with 'remove' allow/deny, delete the file, and return // 17 + FS.Utility.validateAction(ref.collection.files._validators['remove'], ref.file, self.userId); // 18 + // 19 + /* // 20 + * From the DELETE spec: // 21 + * A successful response SHOULD be 200 (OK) if the response includes an // 22 + * entity describing the status, 202 (Accepted) if the action has not // 23 + * yet been enacted, or 204 (No Content) if the action has been enacted // 24 + * but the response does not include an entity. // 25 + */ // 26 + self.setStatusCode(200); // 27 + // 28 + return { // 29 + deleted: !!ref.file.remove() // 30 + }; // 31 +}; // 32 + // 33 +/** // 34 + * @method FS.HTTP.Handlers.GetList // 35 + * @public // 36 + * @returns {Object} response // 37 + * // 38 + * HTTP GET file list request handler // 39 + */ // 40 +FS.HTTP.Handlers.GetList = function httpGetListHandler() { // 41 + // Not Yet Implemented // 42 + // Need to check publications and return file list based on // 43 + // what user is allowed to see // 44 +}; // 45 + // 46 +/* // 47 + requestRange will parse the range set in request header - if not possible it // 48 + will throw fitting errors and autofill range for both partial and full ranges // 49 + // 50 + throws error or returns the object: // 51 + { // 52 + start // 53 + end // 54 + length // 55 + unit // 56 + partial // 57 + } // 58 +*/ // 59 +var requestRange = function(req, fileSize) { // 60 + if (req) { // 61 + if (req.headers) { // 62 + var rangeString = req.headers.range; // 63 + // 64 + // Make sure range is a string // 65 + if (rangeString === ''+rangeString) { // 66 + // 67 + // range will be in the format "bytes=0-32767" // 68 + var parts = rangeString.split('='); // 69 + var unit = parts[0]; // 70 + // 71 + // Make sure parts consists of two strings and range is of type "byte" // 72 + if (parts.length == 2 && unit == 'bytes') { // 73 + // Parse the range // 74 + var range = parts[1].split('-'); // 75 + var start = Number(range[0]); // 76 + var end = Number(range[1]); // 77 + // 78 + // Fix invalid ranges? // 79 + if (range[0] != start) start = 0; // 80 + if (range[1] != end || !end) end = fileSize - 1; // 81 + // 82 + // Make sure range consists of a start and end point of numbers and start is less than end // 83 + if (start < end) { // 84 + // 85 + var partSize = 0 - start + end + 1; // 86 + // 87 + // Return the parsed range // 88 + return { // 89 + start: start, // 90 + end: end, // 91 + length: partSize, // 92 + size: fileSize, // 93 + unit: unit, // 94 + partial: (partSize < fileSize) // 95 + }; // 96 + // 97 + } else { // 98 + throw new Meteor.Error(416, "Requested Range Not Satisfiable"); // 99 + } // 100 + // 101 + } else { // 102 + // The first part should be bytes // 103 + throw new Meteor.Error(416, "Requested Range Unit Not Satisfiable"); // 104 + } // 105 + // 106 + } else { // 107 + // No range found // 108 + } // 109 + // 110 + } else { // 111 + // throw new Error('No request headers set for _parseRange function'); // 112 + } // 113 + } else { // 114 + throw new Error('No request object passed to _parseRange function'); // 115 + } // 116 + // 117 + return { // 118 + start: 0, // 119 + end: fileSize - 1, // 120 + length: fileSize, // 121 + size: fileSize, // 122 + unit: 'bytes', // 123 + partial: false // 124 + }; // 125 +}; // 126 + // 127 +/** // 128 + * @method FS.HTTP.Handlers.Get // 129 + * @public // 130 + * @returns {any} response // 131 + * // 132 + * HTTP GET request handler // 133 + */ // 134 +FS.HTTP.Handlers.Get = function httpGetHandler(ref) { // 135 + var self = this; // 136 + // Once we have the file, we can test allow/deny validators // 137 + // XXX: pass on the "share" query eg. ?share=342hkjh23ggj for shared url access? // 138 + FS.Utility.validateAction(ref.collection._validators['download'], ref.file, self.userId /*, self.query.shareId*/); // 139 + // 140 + var storeName = ref.storeName; // 141 + // 142 + // If no storeName was specified, use the first defined storeName // 143 + if (typeof storeName !== "string") { // 144 + // No store handed, we default to primary store // 145 + storeName = ref.collection.primaryStore.name; // 146 + } // 147 + // 148 + // Get the storage reference // 149 + var storage = ref.collection.storesLookup[storeName]; // 150 + // 151 + if (!storage) { // 152 + throw new Meteor.Error(404, "Not Found", 'There is no store "' + storeName + '"'); // 153 + } // 154 + // 155 + // Get the file // 156 + var copyInfo = ref.file.copies[storeName]; // 157 + // 158 + if (!copyInfo) { // 159 + throw new Meteor.Error(404, "Not Found", 'This file was not stored in the ' + storeName + ' store'); // 160 + } // 161 + // 162 + // Set the content type for file // 163 + if (typeof copyInfo.type === "string") { // 164 + self.setContentType(copyInfo.type); // 165 + } else { // 166 + self.setContentType('application/octet-stream'); // 167 + } // 168 + // 169 + // Add 'Content-Disposition' header if requested a download/attachment URL // 170 + if (typeof ref.download !== "undefined") { // 171 + var filename = ref.filename || copyInfo.name; // 172 + self.addHeader('Content-Disposition', 'attachment; filename="' + filename + '"'); // 173 + } else { // 174 + self.addHeader('Content-Disposition', 'inline'); // 175 + } // 176 + // 177 + // Get the contents range from request // 178 + var range = requestRange(self.request, copyInfo.size); // 179 + // 180 + // Some browsers cope better if the content-range header is // 181 + // still included even for the full file being returned. // 182 + self.addHeader('Content-Range', range.unit + ' ' + range.start + '-' + range.end + '/' + range.size); // 183 + // 184 + // If a chunk/range was requested instead of the whole file, serve that' // 185 + if (range.partial) { // 186 + self.setStatusCode(206, 'Partial Content'); // 187 + } else { // 188 + self.setStatusCode(200, 'OK'); // 189 + } // 190 + // 191 + // Add any other global custom headers and collection-specific custom headers // 192 + FS.Utility.each(getHeaders.concat(getHeadersByCollection[ref.collection.name] || []), function(header) { // 193 + self.addHeader(header[0], header[1]); // 194 + }); // 195 + // 196 + // Inform clients about length (or chunk length in case of ranges) // 197 + self.addHeader('Content-Length', range.length); // 198 + // 199 + // Last modified header (updatedAt from file info) // 200 + self.addHeader('Last-Modified', copyInfo.updatedAt.toUTCString()); // 201 + // 202 + // Inform clients that we accept ranges for resumable chunked downloads // 203 + self.addHeader('Accept-Ranges', range.unit); // 204 + // 205 + if (FS.debug) console.log('Read file "' + (ref.filename || copyInfo.name) + '" ' + range.unit + ' ' + range.start + '-' + range.end + '/' + range.size); + // 207 + var readStream = storage.adapter.createReadStream(ref.file, {start: range.start, end: range.end}); // 208 + // 209 + readStream.on('error', function(err) { // 210 + // Send proper error message on get error // 211 + if (err.message && err.statusCode) { // 212 + self.Error(new Meteor.Error(err.statusCode, err.message)); // 213 + } else { // 214 + self.Error(new Meteor.Error(503, 'Service unavailable')); // 215 + } // 216 + }); // 217 + // 218 + readStream.pipe(self.createWriteStream()); // 219 +}; // 220 + // 221 +/** // 222 + * @method FS.HTTP.Handlers.PutInsert // 223 + * @public // 224 + * @returns {Object} response object with _id property // 225 + * // 226 + * HTTP PUT file insert request handler // 227 + */ // 228 +FS.HTTP.Handlers.PutInsert = function httpPutInsertHandler(ref) { // 229 + var self = this; // 230 + var opts = FS.Utility.extend({}, self.query || {}, self.params || {}); // 231 + // 232 + FS.debug && console.log("HTTP PUT (insert) handler"); // 233 + // 234 + // Create the nice FS.File // 235 + var fileObj = new FS.File(); // 236 + // 237 + // Set its name // 238 + fileObj.name(opts.filename || null); // 239 + // 240 + // Attach the readstream as the file's data // 241 + fileObj.attachData(self.createReadStream(), {type: self.requestHeaders['content-type'] || 'application/octet-stream'}); + // 243 + // Validate with insert allow/deny // 244 + FS.Utility.validateAction(ref.collection.files._validators['insert'], fileObj, self.userId); // 245 + // 246 + // Insert file into collection, triggering readStream storage // 247 + ref.collection.insert(fileObj); // 248 + // 249 + // Send response // 250 + self.setStatusCode(200); // 251 + // 252 + // Return the new file id // 253 + return {_id: fileObj._id}; // 254 +}; // 255 + // 256 +/** // 257 + * @method FS.HTTP.Handlers.PutUpdate // 258 + * @public // 259 + * @returns {Object} response object with _id and chunk properties // 260 + * // 261 + * HTTP PUT file update chunk request handler // 262 + */ // 263 +FS.HTTP.Handlers.PutUpdate = function httpPutUpdateHandler(ref) { // 264 + var self = this; // 265 + var opts = FS.Utility.extend({}, self.query || {}, self.params || {}); // 266 + // 267 + var chunk = parseInt(opts.chunk, 10); // 268 + if (isNaN(chunk)) chunk = 0; // 269 + // 270 + FS.debug && console.log("HTTP PUT (update) handler received chunk: ", chunk); // 271 + // 272 + // Validate with insert allow/deny; also mounts and retrieves the file // 273 + FS.Utility.validateAction(ref.collection.files._validators['insert'], ref.file, self.userId); // 274 + // 275 + self.createReadStream().pipe( FS.TempStore.createWriteStream(ref.file, chunk) ); // 276 + // 277 + // Send response // 278 + self.setStatusCode(200); // 279 + // 280 + return { _id: ref.file._id, chunk: chunk }; // 281 +}; // 282 + // 283 +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +}).call(this); + + + + + + +(function () { + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// // +// packages/cfs:access-point/access-point-server.js // +// // +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // +var path = Npm.require("path"); // 1 + // 2 +HTTP.publishFormats({ // 3 + fileRecordFormat: function (input) { // 4 + // Set the method scope content type to json // 5 + this.setContentType('application/json'); // 6 + if (FS.Utility.isArray(input)) { // 7 + return EJSON.stringify(FS.Utility.map(input, function (obj) { // 8 + return FS.Utility.cloneFileRecord(obj); // 9 + })); // 10 + } else { // 11 + return EJSON.stringify(FS.Utility.cloneFileRecord(input)); // 12 + } // 13 + } // 14 +}); // 15 + // 16 +/** // 17 + * @method FS.HTTP.setHeadersForGet // 18 + * @public // 19 + * @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} // 22 + */ // 23 +FS.HTTP.setHeadersForGet = function setHeadersForGet(headers, collections) { // 24 + if (typeof collections === "string") { // 25 + collections = [collections]; // 26 + } // 27 + if (collections) { // 28 + FS.Utility.each(collections, function(collectionName) { // 29 + getHeadersByCollection[collectionName] = headers || []; // 30 + }); // 31 + } else { // 32 + getHeaders = headers || []; // 33 + } // 34 +}; // 35 + // 36 +/** // 37 + * @method FS.HTTP.publish // 38 + * @public // 39 + * @param {FS.Collection} collection // 40 + * @param {Function} func - Publish function that returns a cursor. // 41 + * @returns {undefined} // 42 + * // 43 + * Publishes all documents returned by the cursor at a GET URL // 44 + * with the format baseUrl/record/collectionName. The publish // 45 + * function `this` is similar to normal `Meteor.publish`. // 46 + */ // 47 +FS.HTTP.publish = function fsHttpPublish(collection, func) { // 48 + var name = baseUrl + '/record/' + collection.name; // 49 + // Mount collection listing URL using http-publish package // 50 + HTTP.publish({ // 51 + name: name, // 52 + defaultFormat: 'fileRecordFormat', // 53 + collection: collection, // 54 + collectionGet: true, // 55 + collectionPost: false, // 56 + documentGet: true, // 57 + documentPut: false, // 58 + documentDelete: false // 59 + }, func); // 60 + // 61 + FS.debug && console.log("Registered HTTP method GET URLs:\n\n" + name + '\n' + name + '/:id\n'); // 62 +}; // 63 + // 64 +/** // 65 + * @method FS.HTTP.unpublish // 66 + * @public // 67 + * @param {FS.Collection} collection // 68 + * @returns {undefined} // 69 + * // 70 + * Unpublishes a restpoint created by a call to `FS.HTTP.publish` // 71 + */ // 72 +FS.HTTP.unpublish = function fsHttpUnpublish(collection) { // 73 + // Mount collection listing URL using http-publish package // 74 + HTTP.unpublish(baseUrl + '/record/' + collection.name); // 75 +}; // 76 + // 77 +_existingMountPoints = {}; // 78 + // 79 +/** // 80 + * @method defaultSelectorFunction // 81 + * @private // 82 + * @returns { collection, file } // 83 + * // 84 + * This is the default selector function // 85 + */ // 86 +var defaultSelectorFunction = function() { // 87 + var self = this; // 88 + // Selector function // 89 + // // 90 + // This function will have to return the collection and the // 91 + // file. If file not found undefined is returned - if null is returned the // 92 + // search was not possible // 93 + var opts = FS.Utility.extend({}, self.query || {}, self.params || {}); // 94 + // 95 + // Get the collection name from the url // 96 + var collectionName = opts.collectionName; // 97 + // 98 + // Get the id from the url // 99 + var id = opts.id; // 100 + // 101 + // Get the collection // 102 + var collection = FS._collections[collectionName]; // 103 + // 104 + // Get the file if possible else return null // 105 + var file = (id && collection)? collection.findOne({ _id: id }): null; // 106 + // 107 + // Return the collection and the file // 108 + return { // 109 + collection: collection, // 110 + file: file, // 111 + storeName: opts.store, // 112 + download: opts.download, // 113 + filename: opts.filename // 114 + }; // 115 +}; // 116 + // 117 +/* // 118 + * @method FS.HTTP.mount // 119 + * @public // 120 + * @param {array of string} mountPoints mount points to map rest functinality on // 121 + * @param {function} selector_f [selector] function returns `{ collection, file }` for mount points to work with // 122 + * // 123 +*/ // 124 +FS.HTTP.mount = function(mountPoints, selector_f) { // 125 + // We take mount points as an array and we get a selector function // 126 + var selectorFunction = selector_f || defaultSelectorFunction; // 127 + // 128 + var accessPoint = { // 129 + 'stream': true, // 130 + 'auth': expirationAuth, // 131 + 'post': function(data) { // 132 + // Use the selector for finding the collection and file reference // 133 + var ref = selectorFunction.call(this); // 134 + // 135 + // We dont support post - this would be normal insert eg. of filerecord? // 136 + throw new Meteor.Error(501, "Not implemented", "Post is not supported"); // 137 + }, // 138 + 'put': function(data) { // 139 + // Use the selector for finding the collection and file reference // 140 + var ref = selectorFunction.call(this); // 141 + // 142 + // Make sure we have a collection reference // 143 + if (!ref.collection) // 144 + throw new Meteor.Error(404, "Not Found", "No collection found"); // 145 + // 146 + // Make sure we have a file reference // 147 + if (ref.file === null) { // 148 + // No id supplied so we will create a new FS.File instance and // 149 + // insert the supplied data. // 150 + return FS.HTTP.Handlers.PutInsert.apply(this, [ref]); // 151 + } else { // 152 + if (ref.file) { // 153 + return FS.HTTP.Handlers.PutUpdate.apply(this, [ref]); // 154 + } else { // 155 + throw new Meteor.Error(404, "Not Found", 'No file found'); // 156 + } // 157 + } // 158 + }, // 159 + 'get': function(data) { // 160 + // Use the selector for finding the collection and file reference // 161 + var ref = selectorFunction.call(this); // 162 + // 163 + // Make sure we have a collection reference // 164 + if (!ref.collection) // 165 + throw new Meteor.Error(404, "Not Found", "No collection found"); // 166 + // 167 + // Make sure we have a file reference // 168 + if (ref.file === null) { // 169 + // No id supplied so we will return the published list of files ala // 170 + // http.publish in json format // 171 + return FS.HTTP.Handlers.GetList.apply(this, [ref]); // 172 + } else { // 173 + if (ref.file) { // 174 + return FS.HTTP.Handlers.Get.apply(this, [ref]); // 175 + } else { // 176 + throw new Meteor.Error(404, "Not Found", 'No file found'); // 177 + } // 178 + } // 179 + }, // 180 + 'delete': function(data) { // 181 + // Use the selector for finding the collection and file reference // 182 + var ref = selectorFunction.call(this); // 183 + // 184 + // Make sure we have a collection reference // 185 + if (!ref.collection) // 186 + throw new Meteor.Error(404, "Not Found", "No collection found"); // 187 + // 188 + // Make sure we have a file reference // 189 + if (ref.file) { // 190 + return FS.HTTP.Handlers.Del.apply(this, [ref]); // 191 + } else { // 192 + throw new Meteor.Error(404, "Not Found", 'No file found'); // 193 + } // 194 + } // 195 + }; // 196 + // 197 + var accessPoints = {}; // 198 + // 199 + // Add debug message // 200 + FS.debug && console.log('Registered HTTP method URLs:'); // 201 + // 202 + FS.Utility.each(mountPoints, function(mountPoint) { // 203 + // Couple mountpoint and accesspoint // 204 + accessPoints[mountPoint] = accessPoint; // 205 + // Remember our mountpoints // 206 + _existingMountPoints[mountPoint] = mountPoint; // 207 + // Add debug message // 208 + FS.debug && console.log(mountPoint); // 209 + }); // 210 + // 211 + // XXX: HTTP:methods should unmount existing mounts in case of overwriting? // 212 + HTTP.methods(accessPoints); // 213 + // 214 +}; // 215 + // 216 +/** // 217 + * @method FS.HTTP.unmount // 218 + * @public // 219 + * @param {string | array of string} [mountPoints] Optional, if not specified all mountpoints are unmounted // 220 + * // 221 + */ // 222 +FS.HTTP.unmount = function(mountPoints) { // 223 + // The mountPoints is optional, can be string or array if undefined then // 224 + // _existingMountPoints will be used // 225 + var unmountList; // 226 + // Container for the mount points to unmount // 227 + var unmountPoints = {}; // 228 + // 229 + if (typeof mountPoints === 'undefined') { // 230 + // Use existing mount points - unmount all // 231 + unmountList = _existingMountPoints; // 232 + } else if (mountPoints === ''+mountPoints) { // 233 + // Got a string // 234 + unmountList = [mountPoints]; // 235 + } else if (mountPoints.length) { // 236 + // Got an array // 237 + unmountList = mountPoints; // 238 + } // 239 + // 240 + // If we have a list to unmount // 241 + if (unmountList) { // 242 + // Iterate over each item // 243 + FS.Utility.each(unmountList, function(mountPoint) { // 244 + // Check _existingMountPoints to make sure the mount point exists in our // 245 + // context / was created by the FS.HTTP.mount // 246 + if (_existingMountPoints[mountPoint]) { // 247 + // Mark as unmount // 248 + unmountPoints[mountPoint] = false; // 249 + // Release // 250 + delete _existingMountPoints[mountPoint]; // 251 + } // 252 + }); // 253 + FS.debug && console.log('FS.HTTP.unmount:'); // 254 + FS.debug && console.log(unmountPoints); // 255 + // Complete unmount // 256 + HTTP.methods(unmountPoints); // 257 + } // 258 +}; // 259 + // 260 +// ### FS.Collection maps on HTTP pr. default on the following restpoints: // 261 +// * // 262 +// baseUrl + '/files/:collectionName/:id/:filename', // 263 +// baseUrl + '/files/:collectionName/:id', // 264 +// baseUrl + '/files/:collectionName' // 265 +// // 266 +// Change/ replace the existing mount point by: // 267 +// ```js // 268 +// // unmount all existing // 269 +// FS.HTTP.unmount(); // 270 +// // Create new mount point // 271 +// FS.HTTP.mount([ // 272 +// '/cfs/files/:collectionName/:id/:filename', // 273 +// '/cfs/files/:collectionName/:id', // 274 +// '/cfs/files/:collectionName' // 275 +// ]); // 276 +// ``` // 277 +// // 278 +mountUrls = function mountUrls() { // 279 + // We unmount first in case we are calling this a second time // 280 + FS.HTTP.unmount(); // 281 + // 282 + FS.HTTP.mount([ // 283 + baseUrl + '/files/:collectionName/:id/:filename', // 284 + baseUrl + '/files/:collectionName/:id', // 285 + baseUrl + '/files/:collectionName' // 286 + ]); // 287 +}; // 288 + // 289 +// Returns the userId from URL token // 290 +var expirationAuth = function expirationAuth() { // 291 + var self = this; // 292 + // 293 + // Read the token from '/hello?token=base64' // 294 + var encodedToken = self.query.token; // 295 + // 296 + FS.debug && console.log("token: "+encodedToken); // 297 + // 298 + if (!encodedToken || !Meteor.users) return false; // 299 + // 300 + // Check the userToken before adding it to the db query // 301 + // Set the this.userId // 302 + var tokenString = FS.Utility.atob(encodedToken); // 303 + // 304 + var tokenObject; // 305 + try { // 306 + tokenObject = JSON.parse(tokenString); // 307 + } catch(err) { // 308 + throw new Meteor.Error(400, 'Bad Request'); // 309 + } // 310 + // 311 + // XXX: Do some check here of the object // 312 + var userToken = tokenObject.authToken; // 313 + if (userToken !== ''+userToken) { // 314 + throw new Meteor.Error(400, 'Bad Request'); // 315 + } // 316 + // 317 + // If we have an expiration token we should check that it's still valid // 318 + if (tokenObject.expiration != null) { // 319 + // check if its too old // 320 + var now = Date.now(); // 321 + if (tokenObject.expiration < now) { // 322 + FS.debug && console.log('Expired token: ' + tokenObject.expiration + ' is less than ' + now); // 323 + throw new Meteor.Error(500, 'Expired token'); // 324 + } // 325 + } // 326 + // 327 + // We are not on a secure line - so we have to look up the user... // 328 + var user = Meteor.users.findOne({ // 329 + $or: [ // 330 + {'services.resume.loginTokens.hashedToken': Accounts._hashLoginToken(userToken)}, // 331 + {'services.resume.loginTokens.token': userToken} // 332 + ] // 333 + }); // 334 + // 335 + // Set the userId in the scope // 336 + return user && user._id; // 337 +}; // 338 + // 339 +HTTP.methods( // 340 + {'/cfs/servertime': { // 341 + get: function(data) { // 342 + return Date.now().toString(); // 343 + } // 344 + } // 345 +}); // 346 + // 347 +// Unify client / server api // 348 +FS.HTTP.now = function() { // 349 + return Date.now(); // 350 +}; // 351 + // 352 +// Start up the basic mount points // 353 +Meteor.startup(function () { // 354 + mountUrls(); // 355 +}); // 356 + // 357 +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +}).call(this); diff --git a/packages/wekan-cfs-access-point/os.json b/packages/wekan-cfs-access-point/os.json new file mode 100644 index 00000000..b6a30553 --- /dev/null +++ b/packages/wekan-cfs-access-point/os.json @@ -0,0 +1,51 @@ +{ + "format": "isopack-2-unibuild", + "declaredExports": [], + "uses": [ + { + "package": "meteor" + }, + { + "package": "cfs:base-package", + "constraint": "0.0.30" + }, + { + "package": "cfs:file", + "constraint": "0.1.16" + }, + { + "package": "check", + "constraint": "1.0.2" + }, + { + "package": "ejson", + "constraint": "1.0.4" + }, + { + "package": "cfs:http-methods", + "constraint": "0.0.29" + }, + { + "package": "cfs:http-publish", + "constraint": "0.0.13" + } + ], + "implies": [ + { + "package": "cfs:base-package", + "constraint": "" + } + ], + "resources": [ + { + "type": "source", + "extension": "js", + "file": "os/packages/cfs_access-point.js", + "length": 104737, + "offset": 0, + "usesDefaultSourceProcessor": true, + "path": "/packages/cfs_access-point.js", + "hash": "cc531dd3645fb5f9a05adf17807f16d7b7abc493" + } + ] +} \ No newline at end of file diff --git a/packages/wekan-cfs-access-point/os/packages/cfs_access-point.js b/packages/wekan-cfs-access-point/os/packages/cfs_access-point.js new file mode 100644 index 00000000..d7a35359 --- /dev/null +++ b/packages/wekan-cfs-access-point/os/packages/cfs_access-point.js @@ -0,0 +1,880 @@ +(function () { + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// // +// packages/cfs:access-point/access-point-common.js // +// // +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // +rootUrlPathPrefix = __meteor_runtime_config__.ROOT_URL_PATH_PREFIX || ""; // 1 +// Adjust the rootUrlPathPrefix if necessary // 2 +if (rootUrlPathPrefix.length > 0) { // 3 + if (rootUrlPathPrefix.slice(0, 1) !== '/') { // 4 + rootUrlPathPrefix = '/' + rootUrlPathPrefix; // 5 + } // 6 + if (rootUrlPathPrefix.slice(-1) === '/') { // 7 + rootUrlPathPrefix = rootUrlPathPrefix.slice(0, -1); // 8 + } // 9 +} // 10 + // 11 +// prepend ROOT_URL when isCordova // 12 +if (Meteor.isCordova) { // 13 + rootUrlPathPrefix = Meteor.absoluteUrl(rootUrlPathPrefix.replace(/^\/+/, '')).replace(/\/+$/, ''); // 14 +} // 15 + // 16 +baseUrl = '/cfs'; // 17 +FS.HTTP = FS.HTTP || {}; // 18 + // 19 +// Note the upload URL so that client uploader packages know what it is // 20 +FS.HTTP.uploadUrl = rootUrlPathPrefix + baseUrl + '/files'; // 21 + // 22 +/** // 23 + * @method FS.HTTP.setBaseUrl // 24 + * @public // 25 + * @param {String} newBaseUrl - Change the base URL for the HTTP GET and DELETE endpoints. // 26 + * @returns {undefined} // 27 + */ // 28 +FS.HTTP.setBaseUrl = function setBaseUrl(newBaseUrl) { // 29 + // 30 + // Adjust the baseUrl if necessary // 31 + if (newBaseUrl.slice(0, 1) !== '/') { // 32 + newBaseUrl = '/' + newBaseUrl; // 33 + } // 34 + if (newBaseUrl.slice(-1) === '/') { // 35 + newBaseUrl = newBaseUrl.slice(0, -1); // 36 + } // 37 + // 38 + // Update the base URL // 39 + baseUrl = newBaseUrl; // 40 + // 41 + // Change the upload URL so that client uploader packages know what it is // 42 + FS.HTTP.uploadUrl = rootUrlPathPrefix + baseUrl + '/files'; // 43 + // 44 + // Remount URLs with the new baseUrl, unmounting the old, on the server only. // 45 + // If existingMountPoints is empty, then we haven't run the server startup // 46 + // code yet, so this new URL will be used at that point for the initial mount. // 47 + if (Meteor.isServer && !FS.Utility.isEmpty(_existingMountPoints)) { // 48 + mountUrls(); // 49 + } // 50 +}; // 51 + // 52 +/* // 53 + * FS.File extensions // 54 + */ // 55 + // 56 +/** // 57 + * @method FS.File.prototype.url Construct the file url // 58 + * @public // 59 + * @param {Object} [options] // 60 + * @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. // 66 + * @param {String} [options.storing=null] A URL to return while the file is being stored. // 67 + * @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. + * // 69 + * Returns the HTTP URL for getting the file or its metadata. // 70 + */ // 71 +FS.File.prototype.url = function(options) { // 72 + var self = this; // 73 + options = options || {}; // 74 + options = FS.Utility.extend({ // 75 + store: null, // 76 + auth: null, // 77 + download: false, // 78 + metadata: false, // 79 + brokenIsFine: false, // 80 + uploading: null, // return this URL while uploading // 81 + storing: null, // return this URL while storing // 82 + filename: null // override the filename that is shown to the user // 83 + }, options.hash || options); // check for "hash" prop if called as helper // 84 + // 85 + // Primarily useful for displaying a temporary image while uploading an image // 86 + if (options.uploading && !self.isUploaded()) { // 87 + return options.uploading; // 88 + } // 89 + // 90 + if (self.isMounted()) { // 91 + // See if we've stored in the requested store yet // 92 + var storeName = options.store || self.collection.primaryStore.name; // 93 + if (!self.hasStored(storeName)) { // 94 + if (options.storing) { // 95 + return options.storing; // 96 + } else if (!options.brokenIsFine) { // 97 + // We want to return null if we know the URL will be a broken // 98 + // link because then we can avoid rendering broken links, broken // 99 + // images, etc. // 100 + return null; // 101 + } // 102 + } // 103 + // 104 + // Add filename to end of URL if we can determine one // 105 + var filename = options.filename || self.name({store: storeName}); // 106 + if (typeof filename === "string" && filename.length) { // 107 + filename = '/' + filename; // 108 + } else { // 109 + filename = ''; // 110 + } // 111 + // 112 + // TODO: Could we somehow figure out if the collection requires login? // 113 + var authToken = ''; // 114 + if (Meteor.isClient && typeof Accounts !== "undefined" && typeof Accounts._storedLoginToken === "function") { // 115 + if (options.auth !== false) { // 116 + // Add reactive deps on the user // 117 + Meteor.userId(); // 118 + // 119 + var authObject = { // 120 + authToken: Accounts._storedLoginToken() || '' // 121 + }; // 122 + // 123 + // If it's a number, we use that as the expiration time (in seconds) // 124 + if (options.auth === +options.auth) { // 125 + authObject.expiration = FS.HTTP.now() + options.auth * 1000; // 126 + } // 127 + // 128 + // Set the authToken // 129 + var authString = JSON.stringify(authObject); // 130 + authToken = FS.Utility.btoa(authString); // 131 + } // 132 + } else if (typeof options.auth === "string") { // 133 + // If the user supplies auth token the user will be responsible for // 134 + // updating // 135 + authToken = options.auth; // 136 + } // 137 + // 138 + // Construct query string // 139 + var params = {}; // 140 + if (authToken !== '') { // 141 + params.token = authToken; // 142 + } // 143 + if (options.download) { // 144 + params.download = true; // 145 + } // 146 + if (options.store) { // 147 + // We use options.store here instead of storeName because we want to omit the queryString // 148 + // whenever possible, allowing users to have "clean" URLs if they want. The server will // 149 + // assume the first store defined on the server, which means that we are assuming that // 150 + // the first on the client is also the first on the server. If that's not the case, the // 151 + // store option should be supplied. // 152 + params.store = options.store; // 153 + } // 154 + var queryString = FS.Utility.encodeParams(params); // 155 + if (queryString.length) { // 156 + queryString = '?' + queryString; // 157 + } // 158 + // 159 + // Determine which URL to use // 160 + var area; // 161 + if (options.metadata) { // 162 + area = '/record'; // 163 + } else { // 164 + area = '/files'; // 165 + } // 166 + // 167 + // Construct and return the http method url // 168 + return rootUrlPathPrefix + baseUrl + area + '/' + self.collection.name + '/' + self._id + filename + queryString; // 169 + } // 170 + // 171 +}; // 172 + // 173 + // 174 + // 175 +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +}).call(this); + + + + + + +(function () { + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// // +// packages/cfs:access-point/access-point-handlers.js // +// // +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // +getHeaders = []; // 1 +getHeadersByCollection = {}; // 2 + // 3 +FS.HTTP.Handlers = {}; // 4 + // 5 +/** // 6 + * @method FS.HTTP.Handlers.Del // 7 + * @public // 8 + * @returns {any} response // 9 + * // 10 + * HTTP DEL request handler // 11 + */ // 12 +FS.HTTP.Handlers.Del = function httpDelHandler(ref) { // 13 + var self = this; // 14 + var opts = FS.Utility.extend({}, self.query || {}, self.params || {}); // 15 + // 16 + // If DELETE request, validate with 'remove' allow/deny, delete the file, and return // 17 + FS.Utility.validateAction(ref.collection.files._validators['remove'], ref.file, self.userId); // 18 + // 19 + /* // 20 + * From the DELETE spec: // 21 + * A successful response SHOULD be 200 (OK) if the response includes an // 22 + * entity describing the status, 202 (Accepted) if the action has not // 23 + * yet been enacted, or 204 (No Content) if the action has been enacted // 24 + * but the response does not include an entity. // 25 + */ // 26 + self.setStatusCode(200); // 27 + // 28 + return { // 29 + deleted: !!ref.file.remove() // 30 + }; // 31 +}; // 32 + // 33 +/** // 34 + * @method FS.HTTP.Handlers.GetList // 35 + * @public // 36 + * @returns {Object} response // 37 + * // 38 + * HTTP GET file list request handler // 39 + */ // 40 +FS.HTTP.Handlers.GetList = function httpGetListHandler() { // 41 + // Not Yet Implemented // 42 + // Need to check publications and return file list based on // 43 + // what user is allowed to see // 44 +}; // 45 + // 46 +/* // 47 + requestRange will parse the range set in request header - if not possible it // 48 + will throw fitting errors and autofill range for both partial and full ranges // 49 + // 50 + throws error or returns the object: // 51 + { // 52 + start // 53 + end // 54 + length // 55 + unit // 56 + partial // 57 + } // 58 +*/ // 59 +var requestRange = function(req, fileSize) { // 60 + if (req) { // 61 + if (req.headers) { // 62 + var rangeString = req.headers.range; // 63 + // 64 + // Make sure range is a string // 65 + if (rangeString === ''+rangeString) { // 66 + // 67 + // range will be in the format "bytes=0-32767" // 68 + var parts = rangeString.split('='); // 69 + var unit = parts[0]; // 70 + // 71 + // Make sure parts consists of two strings and range is of type "byte" // 72 + if (parts.length == 2 && unit == 'bytes') { // 73 + // Parse the range // 74 + var range = parts[1].split('-'); // 75 + var start = Number(range[0]); // 76 + var end = Number(range[1]); // 77 + // 78 + // Fix invalid ranges? // 79 + if (range[0] != start) start = 0; // 80 + if (range[1] != end || !end) end = fileSize - 1; // 81 + // 82 + // Make sure range consists of a start and end point of numbers and start is less than end // 83 + if (start < end) { // 84 + // 85 + var partSize = 0 - start + end + 1; // 86 + // 87 + // Return the parsed range // 88 + return { // 89 + start: start, // 90 + end: end, // 91 + length: partSize, // 92 + size: fileSize, // 93 + unit: unit, // 94 + partial: (partSize < fileSize) // 95 + }; // 96 + // 97 + } else { // 98 + throw new Meteor.Error(416, "Requested Range Not Satisfiable"); // 99 + } // 100 + // 101 + } else { // 102 + // The first part should be bytes // 103 + throw new Meteor.Error(416, "Requested Range Unit Not Satisfiable"); // 104 + } // 105 + // 106 + } else { // 107 + // No range found // 108 + } // 109 + // 110 + } else { // 111 + // throw new Error('No request headers set for _parseRange function'); // 112 + } // 113 + } else { // 114 + throw new Error('No request object passed to _parseRange function'); // 115 + } // 116 + // 117 + return { // 118 + start: 0, // 119 + end: fileSize - 1, // 120 + length: fileSize, // 121 + size: fileSize, // 122 + unit: 'bytes', // 123 + partial: false // 124 + }; // 125 +}; // 126 + // 127 +/** // 128 + * @method FS.HTTP.Handlers.Get // 129 + * @public // 130 + * @returns {any} response // 131 + * // 132 + * HTTP GET request handler // 133 + */ // 134 +FS.HTTP.Handlers.Get = function httpGetHandler(ref) { // 135 + var self = this; // 136 + // Once we have the file, we can test allow/deny validators // 137 + // XXX: pass on the "share" query eg. ?share=342hkjh23ggj for shared url access? // 138 + FS.Utility.validateAction(ref.collection._validators['download'], ref.file, self.userId /*, self.query.shareId*/); // 139 + // 140 + var storeName = ref.storeName; // 141 + // 142 + // If no storeName was specified, use the first defined storeName // 143 + if (typeof storeName !== "string") { // 144 + // No store handed, we default to primary store // 145 + storeName = ref.collection.primaryStore.name; // 146 + } // 147 + // 148 + // Get the storage reference // 149 + var storage = ref.collection.storesLookup[storeName]; // 150 + // 151 + if (!storage) { // 152 + throw new Meteor.Error(404, "Not Found", 'There is no store "' + storeName + '"'); // 153 + } // 154 + // 155 + // Get the file // 156 + var copyInfo = ref.file.copies[storeName]; // 157 + // 158 + if (!copyInfo) { // 159 + throw new Meteor.Error(404, "Not Found", 'This file was not stored in the ' + storeName + ' store'); // 160 + } // 161 + // 162 + // Set the content type for file // 163 + if (typeof copyInfo.type === "string") { // 164 + self.setContentType(copyInfo.type); // 165 + } else { // 166 + self.setContentType('application/octet-stream'); // 167 + } // 168 + // 169 + // Add 'Content-Disposition' header if requested a download/attachment URL // 170 + if (typeof ref.download !== "undefined") { // 171 + var filename = ref.filename || copyInfo.name; // 172 + self.addHeader('Content-Disposition', 'attachment; filename="' + filename + '"'); // 173 + } else { // 174 + self.addHeader('Content-Disposition', 'inline'); // 175 + } // 176 + // 177 + // Get the contents range from request // 178 + var range = requestRange(self.request, copyInfo.size); // 179 + // 180 + // Some browsers cope better if the content-range header is // 181 + // still included even for the full file being returned. // 182 + self.addHeader('Content-Range', range.unit + ' ' + range.start + '-' + range.end + '/' + range.size); // 183 + // 184 + // If a chunk/range was requested instead of the whole file, serve that' // 185 + if (range.partial) { // 186 + self.setStatusCode(206, 'Partial Content'); // 187 + } else { // 188 + self.setStatusCode(200, 'OK'); // 189 + } // 190 + // 191 + // Add any other global custom headers and collection-specific custom headers // 192 + FS.Utility.each(getHeaders.concat(getHeadersByCollection[ref.collection.name] || []), function(header) { // 193 + self.addHeader(header[0], header[1]); // 194 + }); // 195 + // 196 + // Inform clients about length (or chunk length in case of ranges) // 197 + self.addHeader('Content-Length', range.length); // 198 + // 199 + // Last modified header (updatedAt from file info) // 200 + self.addHeader('Last-Modified', copyInfo.updatedAt.toUTCString()); // 201 + // 202 + // Inform clients that we accept ranges for resumable chunked downloads // 203 + self.addHeader('Accept-Ranges', range.unit); // 204 + // 205 + if (FS.debug) console.log('Read file "' + (ref.filename || copyInfo.name) + '" ' + range.unit + ' ' + range.start + '-' + range.end + '/' + range.size); + // 207 + var readStream = storage.adapter.createReadStream(ref.file, {start: range.start, end: range.end}); // 208 + // 209 + readStream.on('error', function(err) { // 210 + // Send proper error message on get error // 211 + if (err.message && err.statusCode) { // 212 + self.Error(new Meteor.Error(err.statusCode, err.message)); // 213 + } else { // 214 + self.Error(new Meteor.Error(503, 'Service unavailable')); // 215 + } // 216 + }); // 217 + // 218 + readStream.pipe(self.createWriteStream()); // 219 +}; // 220 + +const originalHandler = FS.HTTP.Handlers.Get; +FS.HTTP.Handlers.Get = function (ref) { +//console.log(ref.filename); + 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 = 'tempfix'; + } + return originalHandler.call(this, ref); +}; + // 221 +/** // 222 + * @method FS.HTTP.Handlers.PutInsert // 223 + * @public // 224 + * @returns {Object} response object with _id property // 225 + * // 226 + * HTTP PUT file insert request handler // 227 + */ // 228 +FS.HTTP.Handlers.PutInsert = function httpPutInsertHandler(ref) { // 229 + var self = this; // 230 + var opts = FS.Utility.extend({}, self.query || {}, self.params || {}); // 231 + // 232 + FS.debug && console.log("HTTP PUT (insert) handler"); // 233 + // 234 + // Create the nice FS.File // 235 + var fileObj = new FS.File(); // 236 + // 237 + // Set its name // 238 + fileObj.name(opts.filename || null); // 239 + // 240 + // Attach the readstream as the file's data // 241 + fileObj.attachData(self.createReadStream(), {type: self.requestHeaders['content-type'] || 'application/octet-stream'}); + // 243 + // Validate with insert allow/deny // 244 + FS.Utility.validateAction(ref.collection.files._validators['insert'], fileObj, self.userId); // 245 + // 246 + // Insert file into collection, triggering readStream storage // 247 + ref.collection.insert(fileObj); // 248 + // 249 + // Send response // 250 + self.setStatusCode(200); // 251 + // 252 + // Return the new file id // 253 + return {_id: fileObj._id}; // 254 +}; // 255 + // 256 +/** // 257 + * @method FS.HTTP.Handlers.PutUpdate // 258 + * @public // 259 + * @returns {Object} response object with _id and chunk properties // 260 + * // 261 + * HTTP PUT file update chunk request handler // 262 + */ // 263 +FS.HTTP.Handlers.PutUpdate = function httpPutUpdateHandler(ref) { // 264 + var self = this; // 265 + var opts = FS.Utility.extend({}, self.query || {}, self.params || {}); // 266 + // 267 + var chunk = parseInt(opts.chunk, 10); // 268 + if (isNaN(chunk)) chunk = 0; // 269 + // 270 + FS.debug && console.log("HTTP PUT (update) handler received chunk: ", chunk); // 271 + // 272 + // Validate with insert allow/deny; also mounts and retrieves the file // 273 + FS.Utility.validateAction(ref.collection.files._validators['insert'], ref.file, self.userId); // 274 + // 275 + self.createReadStream().pipe( FS.TempStore.createWriteStream(ref.file, chunk) ); // 276 + // 277 + // Send response // 278 + self.setStatusCode(200); // 279 + // 280 + return { _id: ref.file._id, chunk: chunk }; // 281 +}; // 282 + // 283 +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +}).call(this); + + + + + + +(function () { + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// // +// packages/cfs:access-point/access-point-server.js // +// // +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // +var path = Npm.require("path"); // 1 + // 2 +HTTP.publishFormats({ // 3 + fileRecordFormat: function (input) { // 4 + // Set the method scope content type to json // 5 + this.setContentType('application/json'); // 6 + if (FS.Utility.isArray(input)) { // 7 + return EJSON.stringify(FS.Utility.map(input, function (obj) { // 8 + return FS.Utility.cloneFileRecord(obj); // 9 + })); // 10 + } else { // 11 + return EJSON.stringify(FS.Utility.cloneFileRecord(input)); // 12 + } // 13 + } // 14 +}); // 15 + // 16 +/** // 17 + * @method FS.HTTP.setHeadersForGet // 18 + * @public // 19 + * @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} // 22 + */ // 23 +FS.HTTP.setHeadersForGet = function setHeadersForGet(headers, collections) { // 24 + if (typeof collections === "string") { // 25 + collections = [collections]; // 26 + } // 27 + if (collections) { // 28 + FS.Utility.each(collections, function(collectionName) { // 29 + getHeadersByCollection[collectionName] = headers || []; // 30 + }); // 31 + } else { // 32 + getHeaders = headers || []; // 33 + } // 34 +}; // 35 + // 36 +/** // 37 + * @method FS.HTTP.publish // 38 + * @public // 39 + * @param {FS.Collection} collection // 40 + * @param {Function} func - Publish function that returns a cursor. // 41 + * @returns {undefined} // 42 + * // 43 + * Publishes all documents returned by the cursor at a GET URL // 44 + * with the format baseUrl/record/collectionName. The publish // 45 + * function `this` is similar to normal `Meteor.publish`. // 46 + */ // 47 +FS.HTTP.publish = function fsHttpPublish(collection, func) { // 48 + var name = baseUrl + '/record/' + collection.name; // 49 + // Mount collection listing URL using http-publish package // 50 + HTTP.publish({ // 51 + name: name, // 52 + defaultFormat: 'fileRecordFormat', // 53 + collection: collection, // 54 + collectionGet: true, // 55 + collectionPost: false, // 56 + documentGet: true, // 57 + documentPut: false, // 58 + documentDelete: false // 59 + }, func); // 60 + // 61 + FS.debug && console.log("Registered HTTP method GET URLs:\n\n" + name + '\n' + name + '/:id\n'); // 62 +}; // 63 + // 64 +/** // 65 + * @method FS.HTTP.unpublish // 66 + * @public // 67 + * @param {FS.Collection} collection // 68 + * @returns {undefined} // 69 + * // 70 + * Unpublishes a restpoint created by a call to `FS.HTTP.publish` // 71 + */ // 72 +FS.HTTP.unpublish = function fsHttpUnpublish(collection) { // 73 + // Mount collection listing URL using http-publish package // 74 + HTTP.unpublish(baseUrl + '/record/' + collection.name); // 75 +}; // 76 + // 77 +_existingMountPoints = {}; // 78 + // 79 +/** // 80 + * @method defaultSelectorFunction // 81 + * @private // 82 + * @returns { collection, file } // 83 + * // 84 + * This is the default selector function // 85 + */ // 86 +var defaultSelectorFunction = function() { // 87 + var self = this; // 88 + // Selector function // 89 + // // 90 + // This function will have to return the collection and the // 91 + // file. If file not found undefined is returned - if null is returned the // 92 + // search was not possible // 93 + var opts = FS.Utility.extend({}, self.query || {}, self.params || {}); // 94 + // 95 + // Get the collection name from the url // 96 + var collectionName = opts.collectionName; // 97 + // 98 + // Get the id from the url // 99 + var id = opts.id; // 100 + // 101 + // Get the collection // 102 + var collection = FS._collections[collectionName]; // 103 + // 104 + // Get the file if possible else return null // 105 + var file = (id && collection)? collection.findOne({ _id: id }): null; // 106 + // 107 + // Return the collection and the file // 108 + return { // 109 + collection: collection, // 110 + file: file, // 111 + storeName: opts.store, // 112 + download: opts.download, // 113 + filename: opts.filename // 114 + }; // 115 +}; // 116 + // 117 +/* // 118 + * @method FS.HTTP.mount // 119 + * @public // 120 + * @param {array of string} mountPoints mount points to map rest functinality on // 121 + * @param {function} selector_f [selector] function returns `{ collection, file }` for mount points to work with // 122 + * // 123 +*/ // 124 +FS.HTTP.mount = function(mountPoints, selector_f) { // 125 + // We take mount points as an array and we get a selector function // 126 + var selectorFunction = selector_f || defaultSelectorFunction; // 127 + // 128 + var accessPoint = { // 129 + 'stream': true, // 130 + 'auth': expirationAuth, // 131 + 'post': function(data) { // 132 + // Use the selector for finding the collection and file reference // 133 + var ref = selectorFunction.call(this); // 134 + // 135 + // We dont support post - this would be normal insert eg. of filerecord? // 136 + throw new Meteor.Error(501, "Not implemented", "Post is not supported"); // 137 + }, // 138 + 'put': function(data) { // 139 + // Use the selector for finding the collection and file reference // 140 + var ref = selectorFunction.call(this); // 141 + // 142 + // Make sure we have a collection reference // 143 + if (!ref.collection) // 144 + throw new Meteor.Error(404, "Not Found", "No collection found"); // 145 + // 146 + // Make sure we have a file reference // 147 + if (ref.file === null) { // 148 + // No id supplied so we will create a new FS.File instance and // 149 + // insert the supplied data. // 150 + return FS.HTTP.Handlers.PutInsert.apply(this, [ref]); // 151 + } else { // 152 + if (ref.file) { // 153 + return FS.HTTP.Handlers.PutUpdate.apply(this, [ref]); // 154 + } else { // 155 + throw new Meteor.Error(404, "Not Found", 'No file found'); // 156 + } // 157 + } // 158 + }, // 159 + 'get': function(data) { // 160 + // Use the selector for finding the collection and file reference // 161 + var ref = selectorFunction.call(this); // 162 + // 163 + // Make sure we have a collection reference // 164 + if (!ref.collection) // 165 + throw new Meteor.Error(404, "Not Found", "No collection found"); // 166 + // 167 + // Make sure we have a file reference // 168 + if (ref.file === null) { // 169 + // No id supplied so we will return the published list of files ala // 170 + // http.publish in json format // 171 + return FS.HTTP.Handlers.GetList.apply(this, [ref]); // 172 + } else { // 173 + if (ref.file) { // 174 + return FS.HTTP.Handlers.Get.apply(this, [ref]); // 175 + } else { // 176 + throw new Meteor.Error(404, "Not Found", 'No file found'); // 177 + } // 178 + } // 179 + }, // 180 + 'delete': function(data) { // 181 + // Use the selector for finding the collection and file reference // 182 + var ref = selectorFunction.call(this); // 183 + // 184 + // Make sure we have a collection reference // 185 + if (!ref.collection) // 186 + throw new Meteor.Error(404, "Not Found", "No collection found"); // 187 + // 188 + // Make sure we have a file reference // 189 + if (ref.file) { // 190 + return FS.HTTP.Handlers.Del.apply(this, [ref]); // 191 + } else { // 192 + throw new Meteor.Error(404, "Not Found", 'No file found'); // 193 + } // 194 + } // 195 + }; // 196 + // 197 + var accessPoints = {}; // 198 + // 199 + // Add debug message // 200 + FS.debug && console.log('Registered HTTP method URLs:'); // 201 + // 202 + FS.Utility.each(mountPoints, function(mountPoint) { // 203 + // Couple mountpoint and accesspoint // 204 + accessPoints[mountPoint] = accessPoint; // 205 + // Remember our mountpoints // 206 + _existingMountPoints[mountPoint] = mountPoint; // 207 + // Add debug message // 208 + FS.debug && console.log(mountPoint); // 209 + }); // 210 + // 211 + // XXX: HTTP:methods should unmount existing mounts in case of overwriting? // 212 + HTTP.methods(accessPoints); // 213 + // 214 +}; // 215 + // 216 +/** // 217 + * @method FS.HTTP.unmount // 218 + * @public // 219 + * @param {string | array of string} [mountPoints] Optional, if not specified all mountpoints are unmounted // 220 + * // 221 + */ // 222 +FS.HTTP.unmount = function(mountPoints) { // 223 + // The mountPoints is optional, can be string or array if undefined then // 224 + // _existingMountPoints will be used // 225 + var unmountList; // 226 + // Container for the mount points to unmount // 227 + var unmountPoints = {}; // 228 + // 229 + if (typeof mountPoints === 'undefined') { // 230 + // Use existing mount points - unmount all // 231 + unmountList = _existingMountPoints; // 232 + } else if (mountPoints === ''+mountPoints) { // 233 + // Got a string // 234 + unmountList = [mountPoints]; // 235 + } else if (mountPoints.length) { // 236 + // Got an array // 237 + unmountList = mountPoints; // 238 + } // 239 + // 240 + // If we have a list to unmount // 241 + if (unmountList) { // 242 + // Iterate over each item // 243 + FS.Utility.each(unmountList, function(mountPoint) { // 244 + // Check _existingMountPoints to make sure the mount point exists in our // 245 + // context / was created by the FS.HTTP.mount // 246 + if (_existingMountPoints[mountPoint]) { // 247 + // Mark as unmount // 248 + unmountPoints[mountPoint] = false; // 249 + // Release // 250 + delete _existingMountPoints[mountPoint]; // 251 + } // 252 + }); // 253 + FS.debug && console.log('FS.HTTP.unmount:'); // 254 + FS.debug && console.log(unmountPoints); // 255 + // Complete unmount // 256 + HTTP.methods(unmountPoints); // 257 + } // 258 +}; // 259 + // 260 +// ### FS.Collection maps on HTTP pr. default on the following restpoints: // 261 +// * // 262 +// baseUrl + '/files/:collectionName/:id/:filename', // 263 +// baseUrl + '/files/:collectionName/:id', // 264 +// baseUrl + '/files/:collectionName' // 265 +// // 266 +// Change/ replace the existing mount point by: // 267 +// ```js // 268 +// // unmount all existing // 269 +// FS.HTTP.unmount(); // 270 +// // Create new mount point // 271 +// FS.HTTP.mount([ // 272 +// '/cfs/files/:collectionName/:id/:filename', // 273 +// '/cfs/files/:collectionName/:id', // 274 +// '/cfs/files/:collectionName' // 275 +// ]); // 276 +// ``` // 277 +// // 278 +mountUrls = function mountUrls() { // 279 + // We unmount first in case we are calling this a second time // 280 + FS.HTTP.unmount(); // 281 + // 282 + FS.HTTP.mount([ // 283 + baseUrl + '/files/:collectionName/:id/:filename', // 284 + baseUrl + '/files/:collectionName/:id', // 285 + baseUrl + '/files/:collectionName' // 286 + ]); // 287 +}; // 288 + // 289 +// Returns the userId from URL token // 290 +var expirationAuth = function expirationAuth() { // 291 + var self = this; // 292 + // 293 + // Read the token from '/hello?token=base64' // 294 + var encodedToken = self.query.token; // 295 + // 296 + FS.debug && console.log("token: "+encodedToken); // 297 + // 298 + if (!encodedToken || !Meteor.users) return false; // 299 + // 300 + // Check the userToken before adding it to the db query // 301 + // Set the this.userId // 302 + var tokenString = FS.Utility.atob(encodedToken); // 303 + // 304 + var tokenObject; // 305 + try { // 306 + tokenObject = JSON.parse(tokenString); // 307 + } catch(err) { // 308 + throw new Meteor.Error(400, 'Bad Request'); // 309 + } // 310 + // 311 + // XXX: Do some check here of the object // 312 + var userToken = tokenObject.authToken; // 313 + if (userToken !== ''+userToken) { // 314 + throw new Meteor.Error(400, 'Bad Request'); // 315 + } // 316 + // 317 + // If we have an expiration token we should check that it's still valid // 318 + if (tokenObject.expiration != null) { // 319 + // check if its too old // 320 + var now = Date.now(); // 321 + if (tokenObject.expiration < now) { // 322 + FS.debug && console.log('Expired token: ' + tokenObject.expiration + ' is less than ' + now); // 323 + throw new Meteor.Error(500, 'Expired token'); // 324 + } // 325 + } // 326 + // 327 + // We are not on a secure line - so we have to look up the user... // 328 + var user = Meteor.users.findOne({ // 329 + $or: [ // 330 + {'services.resume.loginTokens.hashedToken': Accounts._hashLoginToken(userToken)}, // 331 + {'services.resume.loginTokens.token': userToken} // 332 + ] // 333 + }); // 334 + // 335 + // Set the userId in the scope // 336 + return user && user._id; // 337 +}; // 338 + // 339 +HTTP.methods( // 340 + {'/cfs/servertime': { // 341 + get: function(data) { // 342 + return Date.now().toString(); // 343 + } // 344 + } // 345 +}); // 346 + // 347 +// Unify client / server api // 348 +FS.HTTP.now = function() { // 349 + return Date.now(); // 350 +}; // 351 + // 352 +// Start up the basic mount points // 353 +Meteor.startup(function () { // 354 + mountUrls(); // 355 +}); // 356 + // 357 +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +}).call(this); diff --git a/packages/wekan-cfs-access-point/package.js b/packages/wekan-cfs-access-point/package.js new file mode 100644 index 00000000..294e63ea --- /dev/null +++ b/packages/wekan-cfs-access-point/package.js @@ -0,0 +1,8 @@ +// Source: https://github.com/wekan/cfs-access-point + +Package.describe({ + name: 'wekan-cfs-access-point', + summary: "Wekan forked version of cfs-access-point to include unicode file upload fix", + version: "2.0.0", + git: "https://github.com/wekan/markdown.git" +}); diff --git a/packages/wekan-cfs-access-point/unipackage.json b/packages/wekan-cfs-access-point/unipackage.json new file mode 100644 index 00000000..b8506cbb --- /dev/null +++ b/packages/wekan-cfs-access-point/unipackage.json @@ -0,0 +1,25 @@ +{ + "name": "cfs:access-point", + "summary": "CollectionFS, add ddp and http accesspoint capability", + "version": "0.1.49", + "isTest": false, + "plugins": [], + "unibuilds": [ + { + "kind": "main", + "arch": "os", + "path": "os-legacy.json" + }, + { + "kind": "main", + "arch": "web.browser", + "path": "web.browser-legacy.json" + }, + { + "kind": "main", + "arch": "web.cordova", + "path": "web.cordova-legacy.json" + } + ], + "format": "unipackage-pre2" +} \ No newline at end of file diff --git a/packages/wekan-cfs-access-point/web.browser-legacy.json b/packages/wekan-cfs-access-point/web.browser-legacy.json new file mode 100644 index 00000000..2610636d --- /dev/null +++ b/packages/wekan-cfs-access-point/web.browser-legacy.json @@ -0,0 +1,55 @@ +{ + "format": "unipackage-unibuild-pre1", + "uses": [ + { + "package": "meteor" + }, + { + "package": "cfs:base-package", + "constraint": "0.0.30" + }, + { + "package": "cfs:file", + "constraint": "0.1.16" + }, + { + "package": "check", + "constraint": "1.0.2" + }, + { + "package": "ejson", + "constraint": "1.0.4" + }, + { + "package": "cfs:http-methods", + "constraint": "0.0.29" + }, + { + "package": "cfs:http-publish", + "constraint": "0.0.13" + } + ], + "implies": [ + { + "package": "cfs:base-package", + "constraint": "" + } + ], + "resources": [ + { + "type": "prelink", + "file": "web.browser-legacy/packages/cfs_access-point/packages/cfs_access-point.js", + "length": 31303, + "offset": 0, + "servePath": "/packages/cfs_access-point/packages/cfs_access-point.js" + } + ], + "packageVariables": [ + { + "name": "rootUrlPathPrefix" + }, + { + "name": "baseUrl" + } + ] +} \ No newline at end of file diff --git a/packages/wekan-cfs-access-point/web.browser-legacy/packages/cfs_access-point/packages/cfs_access-point.js b/packages/wekan-cfs-access-point/web.browser-legacy/packages/cfs_access-point/packages/cfs_access-point.js new file mode 100644 index 00000000..03a15559 --- /dev/null +++ b/packages/wekan-cfs-access-point/web.browser-legacy/packages/cfs_access-point/packages/cfs_access-point.js @@ -0,0 +1,262 @@ +(function () { + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// // +// packages/cfs:access-point/access-point-common.js // +// // +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // +rootUrlPathPrefix = __meteor_runtime_config__.ROOT_URL_PATH_PREFIX || ""; // 1 +// Adjust the rootUrlPathPrefix if necessary // 2 +if (rootUrlPathPrefix.length > 0) { // 3 + if (rootUrlPathPrefix.slice(0, 1) !== '/') { // 4 + rootUrlPathPrefix = '/' + rootUrlPathPrefix; // 5 + } // 6 + if (rootUrlPathPrefix.slice(-1) === '/') { // 7 + rootUrlPathPrefix = rootUrlPathPrefix.slice(0, -1); // 8 + } // 9 +} // 10 + // 11 +// prepend ROOT_URL when isCordova // 12 +if (Meteor.isCordova) { // 13 + rootUrlPathPrefix = Meteor.absoluteUrl(rootUrlPathPrefix.replace(/^\/+/, '')).replace(/\/+$/, ''); // 14 +} // 15 + // 16 +baseUrl = '/cfs'; // 17 +FS.HTTP = FS.HTTP || {}; // 18 + // 19 +// Note the upload URL so that client uploader packages know what it is // 20 +FS.HTTP.uploadUrl = rootUrlPathPrefix + baseUrl + '/files'; // 21 + // 22 +/** // 23 + * @method FS.HTTP.setBaseUrl // 24 + * @public // 25 + * @param {String} newBaseUrl - Change the base URL for the HTTP GET and DELETE endpoints. // 26 + * @returns {undefined} // 27 + */ // 28 +FS.HTTP.setBaseUrl = function setBaseUrl(newBaseUrl) { // 29 + // 30 + // Adjust the baseUrl if necessary // 31 + if (newBaseUrl.slice(0, 1) !== '/') { // 32 + newBaseUrl = '/' + newBaseUrl; // 33 + } // 34 + if (newBaseUrl.slice(-1) === '/') { // 35 + newBaseUrl = newBaseUrl.slice(0, -1); // 36 + } // 37 + // 38 + // Update the base URL // 39 + baseUrl = newBaseUrl; // 40 + // 41 + // Change the upload URL so that client uploader packages know what it is // 42 + FS.HTTP.uploadUrl = rootUrlPathPrefix + baseUrl + '/files'; // 43 + // 44 + // Remount URLs with the new baseUrl, unmounting the old, on the server only. // 45 + // If existingMountPoints is empty, then we haven't run the server startup // 46 + // code yet, so this new URL will be used at that point for the initial mount. // 47 + if (Meteor.isServer && !FS.Utility.isEmpty(_existingMountPoints)) { // 48 + mountUrls(); // 49 + } // 50 +}; // 51 + // 52 +/* // 53 + * FS.File extensions // 54 + */ // 55 + // 56 +/** // 57 + * @method FS.File.prototype.url Construct the file url // 58 + * @public // 59 + * @param {Object} [options] // 60 + * @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. // 66 + * @param {String} [options.storing=null] A URL to return while the file is being stored. // 67 + * @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. + * // 69 + * Returns the HTTP URL for getting the file or its metadata. // 70 + */ // 71 +FS.File.prototype.url = function(options) { // 72 + var self = this; // 73 + options = options || {}; // 74 + options = FS.Utility.extend({ // 75 + store: null, // 76 + auth: null, // 77 + download: false, // 78 + metadata: false, // 79 + brokenIsFine: false, // 80 + uploading: null, // return this URL while uploading // 81 + storing: null, // return this URL while storing // 82 + filename: null // override the filename that is shown to the user // 83 + }, options.hash || options); // check for "hash" prop if called as helper // 84 + // 85 + // Primarily useful for displaying a temporary image while uploading an image // 86 + if (options.uploading && !self.isUploaded()) { // 87 + return options.uploading; // 88 + } // 89 + // 90 + if (self.isMounted()) { // 91 + // See if we've stored in the requested store yet // 92 + var storeName = options.store || self.collection.primaryStore.name; // 93 + if (!self.hasStored(storeName)) { // 94 + if (options.storing) { // 95 + return options.storing; // 96 + } else if (!options.brokenIsFine) { // 97 + // We want to return null if we know the URL will be a broken // 98 + // link because then we can avoid rendering broken links, broken // 99 + // images, etc. // 100 + return null; // 101 + } // 102 + } // 103 + // 104 + // Add filename to end of URL if we can determine one // 105 + var filename = options.filename || self.name({store: storeName}); // 106 + if (typeof filename === "string" && filename.length) { // 107 + filename = '/' + filename; // 108 + } else { // 109 + filename = ''; // 110 + } // 111 + // 112 + // TODO: Could we somehow figure out if the collection requires login? // 113 + var authToken = ''; // 114 + if (Meteor.isClient && typeof Accounts !== "undefined" && typeof Accounts._storedLoginToken === "function") { // 115 + if (options.auth !== false) { // 116 + // Add reactive deps on the user // 117 + Meteor.userId(); // 118 + // 119 + var authObject = { // 120 + authToken: Accounts._storedLoginToken() || '' // 121 + }; // 122 + // 123 + // If it's a number, we use that as the expiration time (in seconds) // 124 + if (options.auth === +options.auth) { // 125 + authObject.expiration = FS.HTTP.now() + options.auth * 1000; // 126 + } // 127 + // 128 + // Set the authToken // 129 + var authString = JSON.stringify(authObject); // 130 + authToken = FS.Utility.btoa(authString); // 131 + } // 132 + } else if (typeof options.auth === "string") { // 133 + // If the user supplies auth token the user will be responsible for // 134 + // updating // 135 + authToken = options.auth; // 136 + } // 137 + // 138 + // Construct query string // 139 + var params = {}; // 140 + if (authToken !== '') { // 141 + params.token = authToken; // 142 + } // 143 + if (options.download) { // 144 + params.download = true; // 145 + } // 146 + if (options.store) { // 147 + // We use options.store here instead of storeName because we want to omit the queryString // 148 + // whenever possible, allowing users to have "clean" URLs if they want. The server will // 149 + // assume the first store defined on the server, which means that we are assuming that // 150 + // the first on the client is also the first on the server. If that's not the case, the // 151 + // store option should be supplied. // 152 + params.store = options.store; // 153 + } // 154 + var queryString = FS.Utility.encodeParams(params); // 155 + if (queryString.length) { // 156 + queryString = '?' + queryString; // 157 + } // 158 + // 159 + // Determine which URL to use // 160 + var area; // 161 + if (options.metadata) { // 162 + area = '/record'; // 163 + } else { // 164 + area = '/files'; // 165 + } // 166 + // 167 + // Construct and return the http method url // 168 + return rootUrlPathPrefix + baseUrl + area + '/' + self.collection.name + '/' + self._id + filename + queryString; // 169 + } // 170 + // 171 +}; // 172 + // 173 + // 174 + // 175 +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +}).call(this); + + + + + + +(function () { + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// // +// packages/cfs:access-point/access-point-client.js // +// // +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // +FS.HTTP.setHeadersForGet = function setHeadersForGet() { // 1 + // Client Stub // 2 +}; // 3 + // 4 +FS.HTTP.now = function() { // 5 + return new Date(new Date() + FS.HTTP._serverTimeDiff); // 6 +}; // 7 + // 8 +// Returns the localstorage if its found and working // 9 +// TODO: check if this works in IE // 10 +// could use Meteor._localStorage - just needs a rewrite // 11 +FS.HTTP._storage = function() { // 12 + var storage, // 13 + fail, // 14 + uid; // 15 + try { // 16 + uid = "test"; // 17 + (storage = window.localStorage).setItem(uid, uid); // 18 + fail = (storage.getItem(uid) !== uid); // 19 + storage.removeItem(uid); // 20 + if (fail) { // 21 + storage = false; // 22 + } // 23 + } catch(e) { // 24 + console.log("Error initializing storage for FS.HTTP"); // 25 + console.log(e); // 26 + } // 27 + // 28 + return storage; // 29 +}; // 30 + // 31 +// get our storage if found // 32 +FS.HTTP.storage = FS.HTTP._storage(); // 33 + // 34 +FS.HTTP._prefix = 'fsHTTP.'; // 35 + // 36 +FS.HTTP._serverTimeDiff = 0; // Time difference in ms // 37 + // 38 +if (FS.HTTP.storage) { // 39 + // Initialize the FS.HTTP._serverTimeDiff // 40 + FS.HTTP._serverTimeDiff = (1*FS.HTTP.storage.getItem(FS.HTTP._prefix+'timeDiff')) || 0; // 41 + // At client startup we figure out the time difference between server and // 42 + // client time - this includes lag and timezone // 43 + Meteor.startup(function() { // 44 + // Call the server method an get server time // 45 + HTTP.get(rootUrlPathPrefix + '/cfs/servertime', function(error, result) { // 46 + if (!error) { // 47 + // Update our server time diff // 48 + var dateNew = new Date(+result.content); // 49 + FS.HTTP._serverTimeDiff = dateNew - new Date();// - lag or/and timezone // 50 + // Update the localstorage // 51 + FS.HTTP.storage.setItem(FS.HTTP._prefix + 'timeDiff', FS.HTTP._serverTimeDiff); // 52 + } else { // 53 + console.log(error.message); // 54 + } // 55 + }); // EO Server call // 56 + }); // 57 +} // 58 + // 59 +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +}).call(this); diff --git a/packages/wekan-cfs-access-point/web.browser.json b/packages/wekan-cfs-access-point/web.browser.json new file mode 100644 index 00000000..c2a9aeb9 --- /dev/null +++ b/packages/wekan-cfs-access-point/web.browser.json @@ -0,0 +1,51 @@ +{ + "format": "isopack-2-unibuild", + "declaredExports": [], + "uses": [ + { + "package": "meteor" + }, + { + "package": "cfs:base-package", + "constraint": "0.0.30" + }, + { + "package": "cfs:file", + "constraint": "0.1.16" + }, + { + "package": "check", + "constraint": "1.0.2" + }, + { + "package": "ejson", + "constraint": "1.0.4" + }, + { + "package": "cfs:http-methods", + "constraint": "0.0.29" + }, + { + "package": "cfs:http-publish", + "constraint": "0.0.13" + } + ], + "implies": [ + { + "package": "cfs:base-package", + "constraint": "" + } + ], + "resources": [ + { + "type": "source", + "extension": "js", + "file": "web.browser/packages/cfs_access-point.js", + "length": 31303, + "offset": 0, + "usesDefaultSourceProcessor": true, + "path": "/packages/cfs_access-point.js", + "hash": "ae0ad13784d08d8509c420af6fbd8e6d5322f973" + } + ] +} \ No newline at end of file diff --git a/packages/wekan-cfs-access-point/web.browser/packages/cfs_access-point.js b/packages/wekan-cfs-access-point/web.browser/packages/cfs_access-point.js new file mode 100644 index 00000000..03a15559 --- /dev/null +++ b/packages/wekan-cfs-access-point/web.browser/packages/cfs_access-point.js @@ -0,0 +1,262 @@ +(function () { + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// // +// packages/cfs:access-point/access-point-common.js // +// // +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // +rootUrlPathPrefix = __meteor_runtime_config__.ROOT_URL_PATH_PREFIX || ""; // 1 +// Adjust the rootUrlPathPrefix if necessary // 2 +if (rootUrlPathPrefix.length > 0) { // 3 + if (rootUrlPathPrefix.slice(0, 1) !== '/') { // 4 + rootUrlPathPrefix = '/' + rootUrlPathPrefix; // 5 + } // 6 + if (rootUrlPathPrefix.slice(-1) === '/') { // 7 + rootUrlPathPrefix = rootUrlPathPrefix.slice(0, -1); // 8 + } // 9 +} // 10 + // 11 +// prepend ROOT_URL when isCordova // 12 +if (Meteor.isCordova) { // 13 + rootUrlPathPrefix = Meteor.absoluteUrl(rootUrlPathPrefix.replace(/^\/+/, '')).replace(/\/+$/, ''); // 14 +} // 15 + // 16 +baseUrl = '/cfs'; // 17 +FS.HTTP = FS.HTTP || {}; // 18 + // 19 +// Note the upload URL so that client uploader packages know what it is // 20 +FS.HTTP.uploadUrl = rootUrlPathPrefix + baseUrl + '/files'; // 21 + // 22 +/** // 23 + * @method FS.HTTP.setBaseUrl // 24 + * @public // 25 + * @param {String} newBaseUrl - Change the base URL for the HTTP GET and DELETE endpoints. // 26 + * @returns {undefined} // 27 + */ // 28 +FS.HTTP.setBaseUrl = function setBaseUrl(newBaseUrl) { // 29 + // 30 + // Adjust the baseUrl if necessary // 31 + if (newBaseUrl.slice(0, 1) !== '/') { // 32 + newBaseUrl = '/' + newBaseUrl; // 33 + } // 34 + if (newBaseUrl.slice(-1) === '/') { // 35 + newBaseUrl = newBaseUrl.slice(0, -1); // 36 + } // 37 + // 38 + // Update the base URL // 39 + baseUrl = newBaseUrl; // 40 + // 41 + // Change the upload URL so that client uploader packages know what it is // 42 + FS.HTTP.uploadUrl = rootUrlPathPrefix + baseUrl + '/files'; // 43 + // 44 + // Remount URLs with the new baseUrl, unmounting the old, on the server only. // 45 + // If existingMountPoints is empty, then we haven't run the server startup // 46 + // code yet, so this new URL will be used at that point for the initial mount. // 47 + if (Meteor.isServer && !FS.Utility.isEmpty(_existingMountPoints)) { // 48 + mountUrls(); // 49 + } // 50 +}; // 51 + // 52 +/* // 53 + * FS.File extensions // 54 + */ // 55 + // 56 +/** // 57 + * @method FS.File.prototype.url Construct the file url // 58 + * @public // 59 + * @param {Object} [options] // 60 + * @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. // 66 + * @param {String} [options.storing=null] A URL to return while the file is being stored. // 67 + * @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. + * // 69 + * Returns the HTTP URL for getting the file or its metadata. // 70 + */ // 71 +FS.File.prototype.url = function(options) { // 72 + var self = this; // 73 + options = options || {}; // 74 + options = FS.Utility.extend({ // 75 + store: null, // 76 + auth: null, // 77 + download: false, // 78 + metadata: false, // 79 + brokenIsFine: false, // 80 + uploading: null, // return this URL while uploading // 81 + storing: null, // return this URL while storing // 82 + filename: null // override the filename that is shown to the user // 83 + }, options.hash || options); // check for "hash" prop if called as helper // 84 + // 85 + // Primarily useful for displaying a temporary image while uploading an image // 86 + if (options.uploading && !self.isUploaded()) { // 87 + return options.uploading; // 88 + } // 89 + // 90 + if (self.isMounted()) { // 91 + // See if we've stored in the requested store yet // 92 + var storeName = options.store || self.collection.primaryStore.name; // 93 + if (!self.hasStored(storeName)) { // 94 + if (options.storing) { // 95 + return options.storing; // 96 + } else if (!options.brokenIsFine) { // 97 + // We want to return null if we know the URL will be a broken // 98 + // link because then we can avoid rendering broken links, broken // 99 + // images, etc. // 100 + return null; // 101 + } // 102 + } // 103 + // 104 + // Add filename to end of URL if we can determine one // 105 + var filename = options.filename || self.name({store: storeName}); // 106 + if (typeof filename === "string" && filename.length) { // 107 + filename = '/' + filename; // 108 + } else { // 109 + filename = ''; // 110 + } // 111 + // 112 + // TODO: Could we somehow figure out if the collection requires login? // 113 + var authToken = ''; // 114 + if (Meteor.isClient && typeof Accounts !== "undefined" && typeof Accounts._storedLoginToken === "function") { // 115 + if (options.auth !== false) { // 116 + // Add reactive deps on the user // 117 + Meteor.userId(); // 118 + // 119 + var authObject = { // 120 + authToken: Accounts._storedLoginToken() || '' // 121 + }; // 122 + // 123 + // If it's a number, we use that as the expiration time (in seconds) // 124 + if (options.auth === +options.auth) { // 125 + authObject.expiration = FS.HTTP.now() + options.auth * 1000; // 126 + } // 127 + // 128 + // Set the authToken // 129 + var authString = JSON.stringify(authObject); // 130 + authToken = FS.Utility.btoa(authString); // 131 + } // 132 + } else if (typeof options.auth === "string") { // 133 + // If the user supplies auth token the user will be responsible for // 134 + // updating // 135 + authToken = options.auth; // 136 + } // 137 + // 138 + // Construct query string // 139 + var params = {}; // 140 + if (authToken !== '') { // 141 + params.token = authToken; // 142 + } // 143 + if (options.download) { // 144 + params.download = true; // 145 + } // 146 + if (options.store) { // 147 + // We use options.store here instead of storeName because we want to omit the queryString // 148 + // whenever possible, allowing users to have "clean" URLs if they want. The server will // 149 + // assume the first store defined on the server, which means that we are assuming that // 150 + // the first on the client is also the first on the server. If that's not the case, the // 151 + // store option should be supplied. // 152 + params.store = options.store; // 153 + } // 154 + var queryString = FS.Utility.encodeParams(params); // 155 + if (queryString.length) { // 156 + queryString = '?' + queryString; // 157 + } // 158 + // 159 + // Determine which URL to use // 160 + var area; // 161 + if (options.metadata) { // 162 + area = '/record'; // 163 + } else { // 164 + area = '/files'; // 165 + } // 166 + // 167 + // Construct and return the http method url // 168 + return rootUrlPathPrefix + baseUrl + area + '/' + self.collection.name + '/' + self._id + filename + queryString; // 169 + } // 170 + // 171 +}; // 172 + // 173 + // 174 + // 175 +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +}).call(this); + + + + + + +(function () { + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// // +// packages/cfs:access-point/access-point-client.js // +// // +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // +FS.HTTP.setHeadersForGet = function setHeadersForGet() { // 1 + // Client Stub // 2 +}; // 3 + // 4 +FS.HTTP.now = function() { // 5 + return new Date(new Date() + FS.HTTP._serverTimeDiff); // 6 +}; // 7 + // 8 +// Returns the localstorage if its found and working // 9 +// TODO: check if this works in IE // 10 +// could use Meteor._localStorage - just needs a rewrite // 11 +FS.HTTP._storage = function() { // 12 + var storage, // 13 + fail, // 14 + uid; // 15 + try { // 16 + uid = "test"; // 17 + (storage = window.localStorage).setItem(uid, uid); // 18 + fail = (storage.getItem(uid) !== uid); // 19 + storage.removeItem(uid); // 20 + if (fail) { // 21 + storage = false; // 22 + } // 23 + } catch(e) { // 24 + console.log("Error initializing storage for FS.HTTP"); // 25 + console.log(e); // 26 + } // 27 + // 28 + return storage; // 29 +}; // 30 + // 31 +// get our storage if found // 32 +FS.HTTP.storage = FS.HTTP._storage(); // 33 + // 34 +FS.HTTP._prefix = 'fsHTTP.'; // 35 + // 36 +FS.HTTP._serverTimeDiff = 0; // Time difference in ms // 37 + // 38 +if (FS.HTTP.storage) { // 39 + // Initialize the FS.HTTP._serverTimeDiff // 40 + FS.HTTP._serverTimeDiff = (1*FS.HTTP.storage.getItem(FS.HTTP._prefix+'timeDiff')) || 0; // 41 + // At client startup we figure out the time difference between server and // 42 + // client time - this includes lag and timezone // 43 + Meteor.startup(function() { // 44 + // Call the server method an get server time // 45 + HTTP.get(rootUrlPathPrefix + '/cfs/servertime', function(error, result) { // 46 + if (!error) { // 47 + // Update our server time diff // 48 + var dateNew = new Date(+result.content); // 49 + FS.HTTP._serverTimeDiff = dateNew - new Date();// - lag or/and timezone // 50 + // Update the localstorage // 51 + FS.HTTP.storage.setItem(FS.HTTP._prefix + 'timeDiff', FS.HTTP._serverTimeDiff); // 52 + } else { // 53 + console.log(error.message); // 54 + } // 55 + }); // EO Server call // 56 + }); // 57 +} // 58 + // 59 +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +}).call(this); diff --git a/packages/wekan-cfs-access-point/web.cordova-legacy.json b/packages/wekan-cfs-access-point/web.cordova-legacy.json new file mode 100644 index 00000000..a0a87078 --- /dev/null +++ b/packages/wekan-cfs-access-point/web.cordova-legacy.json @@ -0,0 +1,55 @@ +{ + "format": "unipackage-unibuild-pre1", + "uses": [ + { + "package": "meteor" + }, + { + "package": "cfs:base-package", + "constraint": "0.0.30" + }, + { + "package": "cfs:file", + "constraint": "0.1.16" + }, + { + "package": "check", + "constraint": "1.0.2" + }, + { + "package": "ejson", + "constraint": "1.0.4" + }, + { + "package": "cfs:http-methods", + "constraint": "0.0.29" + }, + { + "package": "cfs:http-publish", + "constraint": "0.0.13" + } + ], + "implies": [ + { + "package": "cfs:base-package", + "constraint": "" + } + ], + "resources": [ + { + "type": "prelink", + "file": "web.cordova-legacy/packages/cfs_access-point/packages/cfs_access-point.js", + "length": 31303, + "offset": 0, + "servePath": "/packages/cfs_access-point/packages/cfs_access-point.js" + } + ], + "packageVariables": [ + { + "name": "rootUrlPathPrefix" + }, + { + "name": "baseUrl" + } + ] +} \ No newline at end of file diff --git a/packages/wekan-cfs-access-point/web.cordova-legacy/packages/cfs_access-point/packages/cfs_access-point.js b/packages/wekan-cfs-access-point/web.cordova-legacy/packages/cfs_access-point/packages/cfs_access-point.js new file mode 100644 index 00000000..03a15559 --- /dev/null +++ b/packages/wekan-cfs-access-point/web.cordova-legacy/packages/cfs_access-point/packages/cfs_access-point.js @@ -0,0 +1,262 @@ +(function () { + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// // +// packages/cfs:access-point/access-point-common.js // +// // +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // +rootUrlPathPrefix = __meteor_runtime_config__.ROOT_URL_PATH_PREFIX || ""; // 1 +// Adjust the rootUrlPathPrefix if necessary // 2 +if (rootUrlPathPrefix.length > 0) { // 3 + if (rootUrlPathPrefix.slice(0, 1) !== '/') { // 4 + rootUrlPathPrefix = '/' + rootUrlPathPrefix; // 5 + } // 6 + if (rootUrlPathPrefix.slice(-1) === '/') { // 7 + rootUrlPathPrefix = rootUrlPathPrefix.slice(0, -1); // 8 + } // 9 +} // 10 + // 11 +// prepend ROOT_URL when isCordova // 12 +if (Meteor.isCordova) { // 13 + rootUrlPathPrefix = Meteor.absoluteUrl(rootUrlPathPrefix.replace(/^\/+/, '')).replace(/\/+$/, ''); // 14 +} // 15 + // 16 +baseUrl = '/cfs'; // 17 +FS.HTTP = FS.HTTP || {}; // 18 + // 19 +// Note the upload URL so that client uploader packages know what it is // 20 +FS.HTTP.uploadUrl = rootUrlPathPrefix + baseUrl + '/files'; // 21 + // 22 +/** // 23 + * @method FS.HTTP.setBaseUrl // 24 + * @public // 25 + * @param {String} newBaseUrl - Change the base URL for the HTTP GET and DELETE endpoints. // 26 + * @returns {undefined} // 27 + */ // 28 +FS.HTTP.setBaseUrl = function setBaseUrl(newBaseUrl) { // 29 + // 30 + // Adjust the baseUrl if necessary // 31 + if (newBaseUrl.slice(0, 1) !== '/') { // 32 + newBaseUrl = '/' + newBaseUrl; // 33 + } // 34 + if (newBaseUrl.slice(-1) === '/') { // 35 + newBaseUrl = newBaseUrl.slice(0, -1); // 36 + } // 37 + // 38 + // Update the base URL // 39 + baseUrl = newBaseUrl; // 40 + // 41 + // Change the upload URL so that client uploader packages know what it is // 42 + FS.HTTP.uploadUrl = rootUrlPathPrefix + baseUrl + '/files'; // 43 + // 44 + // Remount URLs with the new baseUrl, unmounting the old, on the server only. // 45 + // If existingMountPoints is empty, then we haven't run the server startup // 46 + // code yet, so this new URL will be used at that point for the initial mount. // 47 + if (Meteor.isServer && !FS.Utility.isEmpty(_existingMountPoints)) { // 48 + mountUrls(); // 49 + } // 50 +}; // 51 + // 52 +/* // 53 + * FS.File extensions // 54 + */ // 55 + // 56 +/** // 57 + * @method FS.File.prototype.url Construct the file url // 58 + * @public // 59 + * @param {Object} [options] // 60 + * @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. // 66 + * @param {String} [options.storing=null] A URL to return while the file is being stored. // 67 + * @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. + * // 69 + * Returns the HTTP URL for getting the file or its metadata. // 70 + */ // 71 +FS.File.prototype.url = function(options) { // 72 + var self = this; // 73 + options = options || {}; // 74 + options = FS.Utility.extend({ // 75 + store: null, // 76 + auth: null, // 77 + download: false, // 78 + metadata: false, // 79 + brokenIsFine: false, // 80 + uploading: null, // return this URL while uploading // 81 + storing: null, // return this URL while storing // 82 + filename: null // override the filename that is shown to the user // 83 + }, options.hash || options); // check for "hash" prop if called as helper // 84 + // 85 + // Primarily useful for displaying a temporary image while uploading an image // 86 + if (options.uploading && !self.isUploaded()) { // 87 + return options.uploading; // 88 + } // 89 + // 90 + if (self.isMounted()) { // 91 + // See if we've stored in the requested store yet // 92 + var storeName = options.store || self.collection.primaryStore.name; // 93 + if (!self.hasStored(storeName)) { // 94 + if (options.storing) { // 95 + return options.storing; // 96 + } else if (!options.brokenIsFine) { // 97 + // We want to return null if we know the URL will be a broken // 98 + // link because then we can avoid rendering broken links, broken // 99 + // images, etc. // 100 + return null; // 101 + } // 102 + } // 103 + // 104 + // Add filename to end of URL if we can determine one // 105 + var filename = options.filename || self.name({store: storeName}); // 106 + if (typeof filename === "string" && filename.length) { // 107 + filename = '/' + filename; // 108 + } else { // 109 + filename = ''; // 110 + } // 111 + // 112 + // TODO: Could we somehow figure out if the collection requires login? // 113 + var authToken = ''; // 114 + if (Meteor.isClient && typeof Accounts !== "undefined" && typeof Accounts._storedLoginToken === "function") { // 115 + if (options.auth !== false) { // 116 + // Add reactive deps on the user // 117 + Meteor.userId(); // 118 + // 119 + var authObject = { // 120 + authToken: Accounts._storedLoginToken() || '' // 121 + }; // 122 + // 123 + // If it's a number, we use that as the expiration time (in seconds) // 124 + if (options.auth === +options.auth) { // 125 + authObject.expiration = FS.HTTP.now() + options.auth * 1000; // 126 + } // 127 + // 128 + // Set the authToken // 129 + var authString = JSON.stringify(authObject); // 130 + authToken = FS.Utility.btoa(authString); // 131 + } // 132 + } else if (typeof options.auth === "string") { // 133 + // If the user supplies auth token the user will be responsible for // 134 + // updating // 135 + authToken = options.auth; // 136 + } // 137 + // 138 + // Construct query string // 139 + var params = {}; // 140 + if (authToken !== '') { // 141 + params.token = authToken; // 142 + } // 143 + if (options.download) { // 144 + params.download = true; // 145 + } // 146 + if (options.store) { // 147 + // We use options.store here instead of storeName because we want to omit the queryString // 148 + // whenever possible, allowing users to have "clean" URLs if they want. The server will // 149 + // assume the first store defined on the server, which means that we are assuming that // 150 + // the first on the client is also the first on the server. If that's not the case, the // 151 + // store option should be supplied. // 152 + params.store = options.store; // 153 + } // 154 + var queryString = FS.Utility.encodeParams(params); // 155 + if (queryString.length) { // 156 + queryString = '?' + queryString; // 157 + } // 158 + // 159 + // Determine which URL to use // 160 + var area; // 161 + if (options.metadata) { // 162 + area = '/record'; // 163 + } else { // 164 + area = '/files'; // 165 + } // 166 + // 167 + // Construct and return the http method url // 168 + return rootUrlPathPrefix + baseUrl + area + '/' + self.collection.name + '/' + self._id + filename + queryString; // 169 + } // 170 + // 171 +}; // 172 + // 173 + // 174 + // 175 +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +}).call(this); + + + + + + +(function () { + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// // +// packages/cfs:access-point/access-point-client.js // +// // +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // +FS.HTTP.setHeadersForGet = function setHeadersForGet() { // 1 + // Client Stub // 2 +}; // 3 + // 4 +FS.HTTP.now = function() { // 5 + return new Date(new Date() + FS.HTTP._serverTimeDiff); // 6 +}; // 7 + // 8 +// Returns the localstorage if its found and working // 9 +// TODO: check if this works in IE // 10 +// could use Meteor._localStorage - just needs a rewrite // 11 +FS.HTTP._storage = function() { // 12 + var storage, // 13 + fail, // 14 + uid; // 15 + try { // 16 + uid = "test"; // 17 + (storage = window.localStorage).setItem(uid, uid); // 18 + fail = (storage.getItem(uid) !== uid); // 19 + storage.removeItem(uid); // 20 + if (fail) { // 21 + storage = false; // 22 + } // 23 + } catch(e) { // 24 + console.log("Error initializing storage for FS.HTTP"); // 25 + console.log(e); // 26 + } // 27 + // 28 + return storage; // 29 +}; // 30 + // 31 +// get our storage if found // 32 +FS.HTTP.storage = FS.HTTP._storage(); // 33 + // 34 +FS.HTTP._prefix = 'fsHTTP.'; // 35 + // 36 +FS.HTTP._serverTimeDiff = 0; // Time difference in ms // 37 + // 38 +if (FS.HTTP.storage) { // 39 + // Initialize the FS.HTTP._serverTimeDiff // 40 + FS.HTTP._serverTimeDiff = (1*FS.HTTP.storage.getItem(FS.HTTP._prefix+'timeDiff')) || 0; // 41 + // At client startup we figure out the time difference between server and // 42 + // client time - this includes lag and timezone // 43 + Meteor.startup(function() { // 44 + // Call the server method an get server time // 45 + HTTP.get(rootUrlPathPrefix + '/cfs/servertime', function(error, result) { // 46 + if (!error) { // 47 + // Update our server time diff // 48 + var dateNew = new Date(+result.content); // 49 + FS.HTTP._serverTimeDiff = dateNew - new Date();// - lag or/and timezone // 50 + // Update the localstorage // 51 + FS.HTTP.storage.setItem(FS.HTTP._prefix + 'timeDiff', FS.HTTP._serverTimeDiff); // 52 + } else { // 53 + console.log(error.message); // 54 + } // 55 + }); // EO Server call // 56 + }); // 57 +} // 58 + // 59 +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +}).call(this); diff --git a/packages/wekan-cfs-access-point/web.cordova.json b/packages/wekan-cfs-access-point/web.cordova.json new file mode 100644 index 00000000..18fc213a --- /dev/null +++ b/packages/wekan-cfs-access-point/web.cordova.json @@ -0,0 +1,51 @@ +{ + "format": "isopack-2-unibuild", + "declaredExports": [], + "uses": [ + { + "package": "meteor" + }, + { + "package": "cfs:base-package", + "constraint": "0.0.30" + }, + { + "package": "cfs:file", + "constraint": "0.1.16" + }, + { + "package": "check", + "constraint": "1.0.2" + }, + { + "package": "ejson", + "constraint": "1.0.4" + }, + { + "package": "cfs:http-methods", + "constraint": "0.0.29" + }, + { + "package": "cfs:http-publish", + "constraint": "0.0.13" + } + ], + "implies": [ + { + "package": "cfs:base-package", + "constraint": "" + } + ], + "resources": [ + { + "type": "source", + "extension": "js", + "file": "web.cordova/packages/cfs_access-point.js", + "length": 31303, + "offset": 0, + "usesDefaultSourceProcessor": true, + "path": "/packages/cfs_access-point.js", + "hash": "ae0ad13784d08d8509c420af6fbd8e6d5322f973" + } + ] +} \ No newline at end of file diff --git a/packages/wekan-cfs-access-point/web.cordova/packages/cfs_access-point.js b/packages/wekan-cfs-access-point/web.cordova/packages/cfs_access-point.js new file mode 100644 index 00000000..03a15559 --- /dev/null +++ b/packages/wekan-cfs-access-point/web.cordova/packages/cfs_access-point.js @@ -0,0 +1,262 @@ +(function () { + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// // +// packages/cfs:access-point/access-point-common.js // +// // +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // +rootUrlPathPrefix = __meteor_runtime_config__.ROOT_URL_PATH_PREFIX || ""; // 1 +// Adjust the rootUrlPathPrefix if necessary // 2 +if (rootUrlPathPrefix.length > 0) { // 3 + if (rootUrlPathPrefix.slice(0, 1) !== '/') { // 4 + rootUrlPathPrefix = '/' + rootUrlPathPrefix; // 5 + } // 6 + if (rootUrlPathPrefix.slice(-1) === '/') { // 7 + rootUrlPathPrefix = rootUrlPathPrefix.slice(0, -1); // 8 + } // 9 +} // 10 + // 11 +// prepend ROOT_URL when isCordova // 12 +if (Meteor.isCordova) { // 13 + rootUrlPathPrefix = Meteor.absoluteUrl(rootUrlPathPrefix.replace(/^\/+/, '')).replace(/\/+$/, ''); // 14 +} // 15 + // 16 +baseUrl = '/cfs'; // 17 +FS.HTTP = FS.HTTP || {}; // 18 + // 19 +// Note the upload URL so that client uploader packages know what it is // 20 +FS.HTTP.uploadUrl = rootUrlPathPrefix + baseUrl + '/files'; // 21 + // 22 +/** // 23 + * @method FS.HTTP.setBaseUrl // 24 + * @public // 25 + * @param {String} newBaseUrl - Change the base URL for the HTTP GET and DELETE endpoints. // 26 + * @returns {undefined} // 27 + */ // 28 +FS.HTTP.setBaseUrl = function setBaseUrl(newBaseUrl) { // 29 + // 30 + // Adjust the baseUrl if necessary // 31 + if (newBaseUrl.slice(0, 1) !== '/') { // 32 + newBaseUrl = '/' + newBaseUrl; // 33 + } // 34 + if (newBaseUrl.slice(-1) === '/') { // 35 + newBaseUrl = newBaseUrl.slice(0, -1); // 36 + } // 37 + // 38 + // Update the base URL // 39 + baseUrl = newBaseUrl; // 40 + // 41 + // Change the upload URL so that client uploader packages know what it is // 42 + FS.HTTP.uploadUrl = rootUrlPathPrefix + baseUrl + '/files'; // 43 + // 44 + // Remount URLs with the new baseUrl, unmounting the old, on the server only. // 45 + // If existingMountPoints is empty, then we haven't run the server startup // 46 + // code yet, so this new URL will be used at that point for the initial mount. // 47 + if (Meteor.isServer && !FS.Utility.isEmpty(_existingMountPoints)) { // 48 + mountUrls(); // 49 + } // 50 +}; // 51 + // 52 +/* // 53 + * FS.File extensions // 54 + */ // 55 + // 56 +/** // 57 + * @method FS.File.prototype.url Construct the file url // 58 + * @public // 59 + * @param {Object} [options] // 60 + * @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. // 66 + * @param {String} [options.storing=null] A URL to return while the file is being stored. // 67 + * @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. + * // 69 + * Returns the HTTP URL for getting the file or its metadata. // 70 + */ // 71 +FS.File.prototype.url = function(options) { // 72 + var self = this; // 73 + options = options || {}; // 74 + options = FS.Utility.extend({ // 75 + store: null, // 76 + auth: null, // 77 + download: false, // 78 + metadata: false, // 79 + brokenIsFine: false, // 80 + uploading: null, // return this URL while uploading // 81 + storing: null, // return this URL while storing // 82 + filename: null // override the filename that is shown to the user // 83 + }, options.hash || options); // check for "hash" prop if called as helper // 84 + // 85 + // Primarily useful for displaying a temporary image while uploading an image // 86 + if (options.uploading && !self.isUploaded()) { // 87 + return options.uploading; // 88 + } // 89 + // 90 + if (self.isMounted()) { // 91 + // See if we've stored in the requested store yet // 92 + var storeName = options.store || self.collection.primaryStore.name; // 93 + if (!self.hasStored(storeName)) { // 94 + if (options.storing) { // 95 + return options.storing; // 96 + } else if (!options.brokenIsFine) { // 97 + // We want to return null if we know the URL will be a broken // 98 + // link because then we can avoid rendering broken links, broken // 99 + // images, etc. // 100 + return null; // 101 + } // 102 + } // 103 + // 104 + // Add filename to end of URL if we can determine one // 105 + var filename = options.filename || self.name({store: storeName}); // 106 + if (typeof filename === "string" && filename.length) { // 107 + filename = '/' + filename; // 108 + } else { // 109 + filename = ''; // 110 + } // 111 + // 112 + // TODO: Could we somehow figure out if the collection requires login? // 113 + var authToken = ''; // 114 + if (Meteor.isClient && typeof Accounts !== "undefined" && typeof Accounts._storedLoginToken === "function") { // 115 + if (options.auth !== false) { // 116 + // Add reactive deps on the user // 117 + Meteor.userId(); // 118 + // 119 + var authObject = { // 120 + authToken: Accounts._storedLoginToken() || '' // 121 + }; // 122 + // 123 + // If it's a number, we use that as the expiration time (in seconds) // 124 + if (options.auth === +options.auth) { // 125 + authObject.expiration = FS.HTTP.now() + options.auth * 1000; // 126 + } // 127 + // 128 + // Set the authToken // 129 + var authString = JSON.stringify(authObject); // 130 + authToken = FS.Utility.btoa(authString); // 131 + } // 132 + } else if (typeof options.auth === "string") { // 133 + // If the user supplies auth token the user will be responsible for // 134 + // updating // 135 + authToken = options.auth; // 136 + } // 137 + // 138 + // Construct query string // 139 + var params = {}; // 140 + if (authToken !== '') { // 141 + params.token = authToken; // 142 + } // 143 + if (options.download) { // 144 + params.download = true; // 145 + } // 146 + if (options.store) { // 147 + // We use options.store here instead of storeName because we want to omit the queryString // 148 + // whenever possible, allowing users to have "clean" URLs if they want. The server will // 149 + // assume the first store defined on the server, which means that we are assuming that // 150 + // the first on the client is also the first on the server. If that's not the case, the // 151 + // store option should be supplied. // 152 + params.store = options.store; // 153 + } // 154 + var queryString = FS.Utility.encodeParams(params); // 155 + if (queryString.length) { // 156 + queryString = '?' + queryString; // 157 + } // 158 + // 159 + // Determine which URL to use // 160 + var area; // 161 + if (options.metadata) { // 162 + area = '/record'; // 163 + } else { // 164 + area = '/files'; // 165 + } // 166 + // 167 + // Construct and return the http method url // 168 + return rootUrlPathPrefix + baseUrl + area + '/' + self.collection.name + '/' + self._id + filename + queryString; // 169 + } // 170 + // 171 +}; // 172 + // 173 + // 174 + // 175 +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +}).call(this); + + + + + + +(function () { + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// // +// packages/cfs:access-point/access-point-client.js // +// // +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // +FS.HTTP.setHeadersForGet = function setHeadersForGet() { // 1 + // Client Stub // 2 +}; // 3 + // 4 +FS.HTTP.now = function() { // 5 + return new Date(new Date() + FS.HTTP._serverTimeDiff); // 6 +}; // 7 + // 8 +// Returns the localstorage if its found and working // 9 +// TODO: check if this works in IE // 10 +// could use Meteor._localStorage - just needs a rewrite // 11 +FS.HTTP._storage = function() { // 12 + var storage, // 13 + fail, // 14 + uid; // 15 + try { // 16 + uid = "test"; // 17 + (storage = window.localStorage).setItem(uid, uid); // 18 + fail = (storage.getItem(uid) !== uid); // 19 + storage.removeItem(uid); // 20 + if (fail) { // 21 + storage = false; // 22 + } // 23 + } catch(e) { // 24 + console.log("Error initializing storage for FS.HTTP"); // 25 + console.log(e); // 26 + } // 27 + // 28 + return storage; // 29 +}; // 30 + // 31 +// get our storage if found // 32 +FS.HTTP.storage = FS.HTTP._storage(); // 33 + // 34 +FS.HTTP._prefix = 'fsHTTP.'; // 35 + // 36 +FS.HTTP._serverTimeDiff = 0; // Time difference in ms // 37 + // 38 +if (FS.HTTP.storage) { // 39 + // Initialize the FS.HTTP._serverTimeDiff // 40 + FS.HTTP._serverTimeDiff = (1*FS.HTTP.storage.getItem(FS.HTTP._prefix+'timeDiff')) || 0; // 41 + // At client startup we figure out the time difference between server and // 42 + // client time - this includes lag and timezone // 43 + Meteor.startup(function() { // 44 + // Call the server method an get server time // 45 + HTTP.get(rootUrlPathPrefix + '/cfs/servertime', function(error, result) { // 46 + if (!error) { // 47 + // Update our server time diff // 48 + var dateNew = new Date(+result.content); // 49 + FS.HTTP._serverTimeDiff = dateNew - new Date();// - lag or/and timezone // 50 + // Update the localstorage // 51 + FS.HTTP.storage.setItem(FS.HTTP._prefix + 'timeDiff', FS.HTTP._serverTimeDiff); // 52 + } else { // 53 + console.log(error.message); // 54 + } // 55 + }); // EO Server call // 56 + }); // 57 +} // 58 + // 59 +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +}).call(this); -- cgit v1.2.3-1-g7c22