merged with wekan master @ v5.38

This commit is contained in:
Stefan Maaßen 2021-07-20 13:33:42 +02:00
commit cb418f5e23
743 changed files with 117634 additions and 43043 deletions

View file

@ -1 +0,0 @@
.build*

0
packages/markdown/README.md Executable file → Normal file
View file

View file

@ -1,9 +0,0 @@
var mark = marked;
mark.setOptions({
gfm: true,
tables: true,
breaks: true
});
Markdown = mark;

View file

@ -1,43 +0,0 @@
# License information
## Contribution License Agreement
If you contribute code to this project, you are implicitly allowing your code
to be distributed under the MIT license. You are also implicitly verifying that
all code is your original work. `</legalese>`
## Marked
Copyright (c) 2011-2018, Christopher Jeffrey (https://github.com/chjj/)
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
## Markdown
Copyright © 2004, John Gruber
http://daringfireball.net/
All rights reserved.
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
* Neither the name “Markdown” nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
This software is provided by the copyright holders and contributors “as is” and any express or implied warranties, including, but not limited to, the implied warranties of merchantability and fitness for a particular purpose are disclaimed. In no event shall the copyright owner or contributors be liable for any direct, indirect, incidental, special, exemplary, or consequential damages (including, but not limited to, procurement of substitute goods or services; loss of use, data, or profits; or business interruption) however caused and on any theory of liability, whether in contract, strict liability, or tort (including negligence or otherwise) arising in any way out of the use of this software, even if advised of the possibility of such damage.

View file

@ -1,76 +0,0 @@
<a href="https://marked.js.org">
<img width="60px" height="60px" src="https://marked.js.org/img/logo-black.svg" align="right" />
</a>
# Marked
[![npm](https://badgen.net/npm/v/marked)](https://www.npmjs.com/package/marked)
[![gzip size](https://badgen.net/badgesize/gzip/https://cdn.jsdelivr.net/npm/marked/marked.min.js)](https://cdn.jsdelivr.net/npm/marked/marked.min.js)
[![install size](https://badgen.net/packagephobia/install/marked)](https://packagephobia.now.sh/result?p=marked)
[![downloads](https://badgen.net/npm/dt/marked)](https://www.npmjs.com/package/marked)
[![dep](https://badgen.net/david/dep/markedjs/marked?label=deps)](https://david-dm.org/markedjs/marked)
[![dev dep](https://badgen.net/david/dev/markedjs/marked?label=devDeps)](https://david-dm.org/markedjs/marked?type=dev)
[![travis](https://badgen.net/travis/markedjs/marked)](https://travis-ci.org/markedjs/marked)
[![snyk](https://snyk.io/test/npm/marked/badge.svg)](https://snyk.io/test/npm/marked)
- ⚡ built for speed
- ⬇️ low-level compiler for parsing markdown without caching or blocking for long periods of time
- ⚖️ light-weight while implementing all markdown features from the supported flavors & specifications
- 🌐 works in a browser, on a server, or from a command line interface (CLI)
## Demo
Checkout the [demo page](https://marked.js.org/demo/) to see marked in action ⛹️
## Docs
Our [documentation pages](https://marked.js.org) are also rendered using marked 💯
Also read about:
* [Options](https://marked.js.org/#/USING_ADVANCED.md)
* [Extensibility](https://marked.js.org/#/USING_PRO.md)
## Installation
**CLI:** `npm install -g marked`
**In-browser:** `npm install marked`
## Usage
### Warning: 🚨 Marked does not [sanitize](https://marked.js.org/#/USING_ADVANCED.md#options) the output HTML. Please use a sanitize library, like [DOMPurify](https://github.com/cure53/DOMPurify) (recommended), [sanitize-html](https://github.com/apostrophecms/sanitize-html) or [insane](https://github.com/bevacqua/insane) on the output HTML! 🚨
**CLI**
``` bash
$ marked -o hello.html
hello world
^D
$ cat hello.html
<p>hello world</p>
```
**Browser**
```html
<!doctype html>
<html>
<head>
<meta charset="utf-8"/>
<title>Marked in the browser</title>
</head>
<body>
<div id="content"></div>
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
<script>
document.getElementById('content').innerHTML =
marked('# Marked in the browser\n\nRendered by **marked**.');
</script>
</body>
</html>
```
## License
Copyright (c) 2011-2018, Christopher Jeffrey. (MIT License)

View file

@ -1,10 +0,0 @@
# Security Policy
The only completely secure system is the one that doesn't exist in the first place.
Having said that, we take the security of Marked very seriously.
## Reporting a Vulnerability
Please disclose potential security issues by email to the project [committers](https://marked.js.org/#/AUTHORS.md) as well as the [listed owners within NPM](https://docs.npmjs.com/cli/owner).
We will provide an initial assessment of security reports within 48 hours and should apply patches within 2 weeks
(also, feel free to contribute a fix for the issue).

View file

@ -1,33 +0,0 @@
{
"extends": "standard",
"plugins": [
"standard"
],
"parserOptions": {
"ecmaVersion": 5,
"sourceType": "script"
},
"rules": {
"semi": ["error", "always"],
"indent": ["error", 2, {
"SwitchCase": 1,
"VariableDeclarator": { "var": 2 },
"outerIIFEBody": 0
}],
"operator-linebreak": ["error", "before", { "overrides": { "=": "after" } }],
"space-before-function-paren": ["error", "never"],
"no-cond-assign": "off",
"no-useless-escape": "off",
"one-var": "off",
"no-control-regex": "off",
"no-prototype-builtins": "off",
"prefer-const": "off",
"no-var": "off"
},
"env": {
"node": true,
"browser": true,
"amd": true
}
}

View file

@ -1,269 +0,0 @@
# Authors
Marked takes an encompassing approach to its community. As such, you can think of these as [concentric circles](https://medium.com/the-node-js-collection/healthy-open-source-967fa8be7951), where each group encompasses the following groups.
<table>
<tbody>
<tr>
<td align="center" valign="top" style="width:32%">
<a href="https://github.com/chjj">
<img width="100" height="100" src="https://github.com/chjj.png?s=150">
</a>
<br>
<a href="https://github.com/chjj">Christopher Jeffrey</a>
<div>Original Author</div>
<small>Started the fire</small>
</td>
<td align="center" valign="top" style="width:32%">
<a href="https://github.com/joshbruce">
<img width="100" height="100" src="https://github.com/joshbruce.png?s=150">
</a>
<br>
<a href="https://joshbruce.com">Josh Bruce</a>
<div>Publisher</div>
<small>Release Wrangler; Humaning Helper; Heckler of Hypertext</small>
</td>
<td align="center" valign="top" style="width:32%">
<a href="https://github.com/styfle">
<img width="100" height="100" src="https://github.com/styfle.png?s=150">
</a>
<br>
<a href="https://www.ceriously.com">Steven</a>
<div>Publisher</div>
<small>Release Wrangler; Dr. Docs; Open source, of course; GitHub Guru; Humaning Helper</small>
</td>
</tr>
<tr>
<td align="center" valign="top">
<a href="https://github.com/davisjam">
<img width="100" height="100" src="https://github.com/davisjam.png?s=150">
</a>
<br>
<a href="https://github.com/davisjam">Jamie Davis</a>
<div>Committer</div>
<small>Seeker of Security</small>
</td>
<td align="center" valign="top">
<a href="https://github.com/UziTech">
<img width="100" height="100" src="https://github.com/UziTech.png?s=150">
</a>
<br>
<a href="https://tony.brix.ninja">Tony Brix</a>
<div>Publisher</div>
<small>Release Wrangler; Titan of the test harness; Dr. DevOps</small>
</td>
<td align="center" valign="top">
&nbsp;
</td>
</tr>
</tbody>
</table>
<table>
<tbody>
<tr>
<td align="center" valign="top">
<a href="https://github.com/intcreator">
<img width="100" height="100" src="https://github.com/intcreator.png?s=150">
</a>
<br>
<a href="https://github.com/intcreator">Brandon der Blätter</a>
<div>Contributor</div>
<small>Curious Contributor</small>
</td>
<td align="center" valign="top">
<a href="https://github.com/carlosvalle">
<img width="100" height="100" src="https://github.com/carlosvalle.png?s=150">
</a>
<br>
<a href="https://github.com/carlosvalle">Carlos Valle</a>
<div>Contributor</div>
<small>Maker of the Marked mark from 2018 to present</small>
</td>
<td align="center" width="20%" valign="top">
<a href="https://github.com/Feder1co5oave">
<img width="100" height="100" src="https://github.com/Feder1co5oave.png?s=150">
</a>
<br>
<a href="https://github.com/Feder1co5oave">Federico Soave</a>
<div>Contributor</div>
<small>Regent of the Regex; Master of Marked</small>
</td>
<td align="center" valign="top">
<a href="https://github.com/karenyavine">
<img width="100" height="100" src="https://github.com/karenyavine.png?s=150">
</a>
<br>
<a href="https://github.com/karenyavine">Karen Yavine</a>
<div>Contributor</div>
<small>Snyk's Security Saint</small>
</td>
</tr>
<tr>
<td align="center" valign="top">
<a href="https://github.com/KostyaTretyak">
<img width="100" height="100" src="https://github.com/KostyaTretyak.png?s=150">
</a>
<br>
<a href="https://github.com/KostyaTretyak">Костя Третяк</a>
<div>Contributor</div>
<small></small>
</td>
<td align="center" width="20%" valign="top">
<a href="https://github.com/tomtheisen">
<img width="100" height="100" src="https://github.com/tomtheisen.png?s=150">
</a>
<br>
<a href="https://github.com/tomtheisen">Tom Theisen</a>
<div>Contributor</div>
<small>Defibrillator</small>
</td>
<td align="center" width="20%" valign="top">
<a href="https://github.com/mccraveiro">
<img width="100" height="100" src="https://github.com/mccraveiro.png?s=150">
</a>
<br>
<a href="https://github.com/mccraveiro">Mateus Craveiro</a>
<div>Contributor</div>
<small>Defibrillator</small>
</td>
<td align="center" width="20%" valign="top">
</td>
</tr>
</tbody>
</table>
## Publishers
Publishers are admins who also have the responsibility, privilege, and burden of publishing the new releases to NPM and performing outreach and external stakeholder communications. Further, when things go pear-shaped, they're the ones taking most of the heat. Finally, when things go well, they're the primary ones praising the contributors who made it possible.
(In other words, while Admins are focused primarily on the internal workings of the project, Publishers are focused on internal *and* external concerns.)
**Should not exceed 2:** Having more people with the authority to publish a release can quickly turn into a consensus seeking nightmare (design by committee). Having only one is preferred (Directly Responsible Individual); however, given the nature of the project and its history, having an immediate fallback, and a potential deep fallback (Original author) is probably a good idea.
[Details on badges](#badges)
## Admins
Admins are committers who also have the responsibility, privilege, and burden of selecting committers and making sure the project itself runs smoothly, which includes community maintenance, governance, dispute resolution, and so on. (Letting the contributors easily enter into, and work within, the project to begin contributing, with as little friction as possible.)
**Should not exceed 3:** When there are too many people with the ability to resolve disputes, the dispute itself can quickly turn into a dispute amongst the admins themselves; therefore, we want this group to be small enough to commit to action and large enough to not put too much burden on one person. (Should ensure faster resolution and responsiveness.)
To be listed: Admins are usually selected from the pool of committers (or they volunteer, using the same process) who demonstrate good understanding of the marked culture, operations, and do their best to help new contributors get up to speed on how to contribute effectively to the project.
To be removed: You can remove yourself through the [GitHub UI](https://help.github.com/articles/removing-yourself-from-a-collaborator-s-repository/).
[Details on badges](#badges)
## Committers
Committers are contributors who also have the responsibility, privilege, some might even say burden of being able to review and merge contributions (just usually not their own).
A note on "decision making authority". This is related to submitting PRs and the [advice process](http://www.reinventingorganizationswiki.com/Decision_Making). The person marked as having decision making authority over a certain area should be sought for advice in that area before committing to a course of action.
**Should not exceed 5:** For larger PRs affecting more of the codebase and, most likely, review by more people, we try to keep this pool small and responsive and let those with decision making authority have final say without negative repercussions from the other committers.
To be listed: Committers are usually selected (or they volunteer, using the same process) from contributors who enter the discussions regarding the future direction of Marked (maybe even doing informal reviews of contributions despite not being able to merge them yourself).
To be removed: You can remove yourself through the [GitHub UI](https://help.github.com/articles/removing-yourself-from-a-collaborator-s-repository/).
A note on volunteering:
1. Please do not volunteer unless you believe you can demonstrate to your peers you can do the work required.
2. Please do not overcommit yourself; we count on those committed to the project to be responsive. Really consider, with all you have going on, whether you able to really commit to it.
3. Don't let the previous frighten you away, it can always be changed later by you or your peers.
[Details on badges](#badges)
## Contributors
Contributors are users who submit a [PR](https://github.com/markedjs/marked/pulls), [Issue](https://github.com/markedjs/marked/issues), or collaborate in making Marked a better product and experience for all the users.
To be listed: make a contribution and, if it has significant impact, the committers may be able to add you here.
To be removed: please let us know or submit a PR.
[Details on badges](#badges)
## Users
Users are anyone using Marked in some fashion, without them, there's no reason for us to exist.
|Individual or Organization |Website |Project |Submitted by |
|:--------------------------|:-----------------------|:------------------------------------|:---------------------------------------------------|
|MarkedJS |https://marked.js.org |https://github.com/markedjs/marked |The marked committers |
To be listed: All fields are optional. Contact any of the committers or, more timely, submit a pull request with the following (using the first row as an example):
- **Individual or Organization:** The name you would like associated with the record.
- **Website:** A URL to a standalone website for the project.
- **Project:** A URL for the repository of the project using marked.
- **Submitted by:** The name and optional honorifics for the person adding the listing.
To be removed: Same as above. Only instead of requesting addition request deletion or delete the row yourself.
<h2 id="badges">Badges</h2>
Badges? You don't *need* no stinkin' badges.
Movie references aside. (It was either that or, "Let's play a game", but that would have been creepy&hellip;that's why it will most likely come later.)
Badges? If you *want* 'em, we got 'em, and here's how you get 'em (and&hellip;dramatic pause&hellip;why not two dramatic pauses for emphasis?&hellip; how they can be taken away).
- [ ] Add the appropriate badge to the desired contributor in the desired column of this page, even if they're not listed here yet.
- [ ] Submit a PR (we're big on PRs around here, if you haven't noticed, help us help you).
- [ ] Follow the instructions for submitting a badge PR. (There are more details to find within. Come on. Everybody likes surprises, right? No? Actually, we just try to put documentation where it belongs, closer to the code and part of the sequence of events.)
### Badges at play:
<dl>
<dt>Curious Contributor</dt>
<dd>A contributor with less than one year on this page who is actively engaged in submitting PRs, Issues, making recommendations, sharing thoughts&hellip;without being too annoying about it (let's be clear, submitting 100 Issues recommending the Marked Committers send everyone candy is trying for the badge, not honestly earning it).</dd>
<dt>Dr. DevOps</dt>
<dd>
<p>Someone who understands and contributes to improving the developer experience and flow of Marked into the world.</p>
<blockquote>
"The main characteristic of the DevOps movement is to strongly advocate automation and monitoring at all steps of software construction, from integration, testing, releasing to deployment and infrastructure management. DevOps aims at shorter development cycles, increased deployment frequency, more dependable releases, in close alignment with business objectives." ~ <a href="https://www.wikipedia.org/wiki/DevOps">Wikipedia</a>
</blockquote>
</dd>
<dt>Dr. Docs</dt>
<dd>Someone who has contributed a great deal to the creation and maintenance of the non-code areas of marked.</dd>
<dt>Eye for the CLI</dt>
<dd>At this point? Pretty much anyone who can update that `man` file to the current Marked version without regression in the CLI tool itself.</dd>
<dt>GitHub Guru</dt>
<dd>Someone who always seems to be able to tell you easier ways to do things with GitHub.</dd>
<dt>Humaning Helper</dt>
<dd>Someone who goes out of their way to help contributors feel welcomed and valued. Further, someone who takes the extra steps(s) necessary to help new contributors get up to speed. Finally, they maintain composure even in times of disagreement and dispute resolution.</dd>
<dt>Heckler of Hypertext</dt>
<dd>Someone who demonstrates an esoteric level of knowledge when it comes to HTML. In other words, someone who says things like, "Did you know most Markdown flavors don't have a way to render a description list (`dl`)? All the more reason Markdown `!==` HTML."</dd>
<dt>Markdown Maestro</dt>
<dd>You know that person who knows about way too many different flavors of Markdown? The one who maybe seems a little too obsessed with the possibilities of Markdown beyond HTML? Come on. You know who they are. Or, at least you could, if you give them this badge.</dd>
<dt>Master of Marked</dt>
<dd>Someone who demonstrates they know the ins and outs of the codebase for Marked.</dd>
<dt>Open source, of course</dt>
<dd>Someone who advocates for and has a proven understanding of how to operate within open source communities.</dd>
<dt>Regent of the Regex</dt>
<dd><p>Can you demonstrate you understand the following without Google and Stackoverflow?</p>
<p><code>/^( *)(bull) [\s\S]+?(?:hr|def|\n{2,}(?! )(?!\1bull )\n*|\s*$)/</code></p>
<p>Because this author can't yet. That's who gets these.</p>
</dd>
<dt>Seeker of Security</dt>
<dd>Someone who has demonstrated a high degree of expertise or authority when it comes to software security.</dd>
<dt>Titan of the Test Harness</dt>
<dd>Someone who demonstrates high-levels of understanding regarding Marked's test harness.</dd>
<dt>Totally Tron</dt>
<dd>Someone who demonstrates they are willing and able to "fight for the users", both developers dependent on marked to do their jobs as well as end-users interacting with the output (particularly in the realm of those with the disabilities).</dd>
</dl>
### Special badges that come with the job:
<dl>
<dt>Defibrillator</dt>
<dd>A contributor who stepped up to help bring Marked back to life by contributing solutions to help Marked pass when compared against the CommonMark and GitHub Flavored Markdown specifications.</dd>
<dt>Maker of the Marked mark</dt>
<dd>This badge is given to the person or organization credited with creating the logo (or logotype) used in Marked communications for a given period of time. **Maker of the Marked mark from 2017 to present**, for example.</dd>
<dt>Release Wrangler</dt>
<dd>This is a badge given to all Publishers.</dd>
<dt>Snyk's Security Saint</dt>
<dd>This is a badge given to whomever primarily reaches out from Snyk to let us know about security issues.</dd>
</dl>

View file

@ -1 +0,0 @@
marked.js.org

View file

@ -1,74 +0,0 @@
# Contributor Covenant Code of Conduct
## Our Pledge
In the interest of fostering an open and welcoming environment, we as
contributors and maintainers pledge to making participation in our project and
our community a harassment-free experience for everyone, regardless of age, body
size, disability, ethnicity, gender identity and expression, level of experience,
nationality, personal appearance, race, religion, or sexual identity and
orientation.
## Our Standards
Examples of behavior that contributes to creating a positive environment
include:
* Using welcoming and inclusive language
* Being respectful of differing viewpoints and experiences
* Gracefully accepting constructive criticism
* Focusing on what is best for the community
* Showing empathy towards other community members
Examples of unacceptable behavior by participants include:
* The use of sexualized language or imagery and unwelcome sexual attention or
advances
* Trolling, insulting/derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or electronic
address, without explicit permission
* Other conduct which could reasonably be considered inappropriate in a
professional setting
## Our Responsibilities
Project maintainers are responsible for clarifying the standards of acceptable
behavior and are expected to take appropriate and fair corrective action in
response to any instances of unacceptable behavior.
Project maintainers have the right and responsibility to remove, edit, or
reject comments, commits, code, wiki edits, issues, and other contributions
that are not aligned to this Code of Conduct, or to block temporarily or
permanently any contributor for other behaviors that they deem inappropriate,
threatening, offensive, or harmful.
## Scope
This Code of Conduct applies both within project spaces and in public spaces
when an individual is representing the project or its community. Examples of
representing a project or community include using an official project e-mail
address, posting via an official social media account, or acting as an appointed
representative at an online or offline event. Representation of a project may be
further defined and clarified by project maintainers.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported by contacting the project team by submitting a PR with changes to the [AUTHORS](#/AUTHORS.md) page (or emailing josh@8fold.com). All
complaints will be reviewed and investigated and will result in a response that
is deemed necessary and appropriate to the circumstances. The project team is
obligated to maintain confidentiality with regard to the reporter of an incident.
Further details of specific enforcement policies may be posted separately.
Project maintainers who do not follow or enforce the Code of Conduct in good
faith may face temporary or permanent repercussions as determined by other
members of the project's leadership.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
version [1.4][version].
[homepage]: https://www.contributor-covenant.org/
[version]: https://www.contributor-covenant.org/version/1/4/code-of-conduct.html

View file

@ -1,95 +0,0 @@
# Contributing to Marked
- [ ] Fork `markedjs/marked`.
- [ ] Clone the library locally using GitHub Desktop or the command line.
- [ ] Make sure you are on the `master` branch.
- [ ] Be sure to run `npm install` or `npm update`.
- [ ] Create a branch.
- [ ] Update code in `src` folder. (`lib` folder is for auto compiled code)
- [ ] Run `npm run test:all`, fix any broken things (for linting, you can run `npm run lint` to have the linter fix them for you).
- [ ] Run `npm run build:reset` to remove changes to compiled files.
- [ ] Submit a Pull Request.
## Design principles
Marked tends to favor following the SOLID set of software design and development principles; mainly the [single responsibility](https://en.wikipedia.org/wiki/Single_responsibility_principle) and [open/closed principles](https://en.wikipedia.org/wiki/Open/closed_principle):
- **Single responsibility:** Marked, and the components of Marked, have the single responsibility of converting Markdown strings into HTML.
- **Open/closed:** Marked favors giving developers the means to easily extend the library and its components over changing Marked's behavior through configuration options.
## Priorities
We think we have our priorities sorted to build quality in.
The following table lists the ticket type labels we use when there is work to be done on the code either through an Issue or a PR; in priority order.
|Ticket type label |Description |
|:----------------------------------|:-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|L0 - security |A security vulnerability within the Marked library is discovered. |
|L1 - broken |Valid usage results in incorrect output compared to [supported specifications](#/README.md#specifications) OR causes marked to crash AND there is no known workaround for the issue. |
|L2 - annoying |Similar to L1 - broken only there is a known workaround available for the issue. |
|RR - refactor and re-engineer |Results in an improvement to developers using Marked (improved readability) or end-users (faster performance) or both. |
|NFS - new feature (spec related) |A capability Marked does not currently provide but is in one of the [supported specifications](#/README.md#specifications) |
|NFU - new feature (user requested) |A capability Marked does not currently provide but has been requested by users of Marked. |
|NFE - new feature (should be an extension) |A capability Marked does not currently provide and is not part of a spec. |
## Test early, often, and everything
We try to write test cases to validate output (writing tests based on the [supported specifications](#/README.md#specifications)) and minimize regression (writing tests for issues fixed). Therefore, if you would like to contribute, some things you should know regarding the test harness.
|Location |Description |
|:---------------------|:--------------------------------------------------------------------------------------------------------------|
|/test/specs/commonmark|Tests for [CommonMark](https://spec.commonmark.org/current/) compliance |
|/test/specs/gfm |Tests for [GFM](https://github.github.com/gfm/) compliance |
|/test/specs/new |Tests not related to the original `markdown.pl`. |
|/test/specs/original |Tests validating against the original `markdown.pl`. |
|/test/specs/redos |Tests for [ReDOS](https://www.owasp.org/index.php/Regular_expression_Denial_of_Service_-_ReDoS) vulnerabilities|
If your test uses features or options, assuming `gfm` is set to `false`, for example, you can add [front-matter](https://www.npmjs.com/package/front-matter) to the top of
your `.md` file
``` yml
---
gfm: false
---
```
## Submitting PRs and Issues
Marked provides templates for submitting both pull requests and issues. When you begin creating a new PR or issue, you will see instructions on using the template.
The PR templates include checklists for both the submitter and the reviewer, which, in most cases, will not be the same person.
## Scripts
When it comes to NPM commands, we try to use the native scripts provided by the NPM framework.
To run the tests:
``` bash
npm test
```
To test whether you are using the standard syntax rules for the project:
```bash
npm run test:lint
```
To see time comparisons between Marked and other popular Markdown libraries:
```bash
npm run bench
```
To check for (and fix) standardized syntax (lint):
```bash
npm run lint
```
To build your own es5, esm, and minified versions of Marked:
```bash
npm run build
```

View file

@ -1,24 +0,0 @@
# Releasing Marked
- [ ] See [contributing](#/CONTRIBUTING.md)
- [ ] Create release branch from `master` (`release-x.y.z`)
- [ ] Submit PR with minimal name: Release x.y.z
- [ ] Complete PR checklists
## Overall strategy
**Master is always shippable:** We try to merge PRs in such a way that `master` is the only branch to really be concerned about *and* `master` can always be released. This allows smoother flow between new features, bug fixes, and so on. (Almost a continuous deployment setup, without automation.)
## Versioning
We follow [semantic versioning](https://semver.org) where the following sequence is true `[major].[minor].[patch]`; therefore, consider the following implications of the release you are preparing:
1. **Major:** There is at least one change not deemed backward compatible.
2. **Minor:** There is at least one new feature added to the release.
3. **Patch:** No breaking changes, no new features.
What to expect while Marked is a zero-major (0.x.y):
1. The major will remain at zero; thereby, alerting consumers to the potentially volatile nature of the package.
2. The minor will tend to be more analogous to a `major` release.
3. The patch will tend to be more analogous to a `minor` release or a collection of bug fixes (patches).

View file

@ -1,85 +0,0 @@
Marked is
1. built for speed.<sup>*</sup>
2. a low-level markdown compiler for parsing markdown without caching or blocking for long periods of time.<sup>**</sup>
3. light-weight while implementing all markdown features from the supported flavors & specifications.<sup>***</sup>
4. available as a command line interface (CLI) and running in client- or server-side JavaScript projects.
<p><small><sup>*</sup> Still working on metrics for comparative analysis and definition.</small><br>
<small><sup>**</sup> As few dependencies as possible.</small><br>
<small><sup>***</sup> Strict compliance could result in slower processing when running comparative benchmarking.</small></p>
<h2 id="demo">Demo</h2>
Checkout the [demo page](./demo/) to see marked in action ⛹️
These documentation pages are also rendered using marked 💯
<h2 id="installation">Installation</h2>
**CLI:** `npm install -g marked`
**In-browser:** `npm install marked`
<h2 id="usage">Usage</h2>
### Warning: 🚨 Marked does not [sanitize](https://marked.js.org/#/USING_ADVANCED.md#options) the output HTML. Please use a sanitize library, like [DOMPurify](https://github.com/cure53/DOMPurify) (recommended), [sanitize-html](https://github.com/apostrophecms/sanitize-html) or [insane](https://github.com/bevacqua/insane) on the output HTML! 🚨
**CLI**
``` bash
$ marked -o hello.html
hello world
^D
$ cat hello.html
<p>hello world</p>
```
``` bash
$ marked -s "*hello world*"
<p><em>hello world</em></p>
```
**Browser**
```html
<!doctype html>
<html>
<head>
<meta charset="utf-8"/>
<title>Marked in the browser</title>
</head>
<body>
<div id="content"></div>
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
<script>
document.getElementById('content').innerHTML =
marked('# Marked in browser\n\nRendered by **marked**.');
</script>
</body>
</html>
```
Marked offers [advanced configurations](#/USING_ADVANCED.md) and [extensibility](#/USING_PRO.md) as well.
<h2 id="specifications">Supported Markdown specifications</h2>
We actively support the features of the following [Markdown flavors](https://github.com/commonmark/CommonMark/wiki/Markdown-Flavors).
|Flavor |Version |
|:----------------------------------------------------------|:----------|
|The original markdown.pl |-- |
|[CommonMark](http://spec.commonmark.org/0.29/) |0.29 |
|[GitHub Flavored Markdown](https://github.github.com/gfm/) |0.29 |
By supporting the above Markdown flavors, it's possible that Marked can help you use other flavors as well; however, these are not actively supported by the community.
<h2 id="security">Security</h2>
The only completely secure system is the one that doesn't exist in the first place. Having said that, we take the security of Marked very seriously.
Therefore, please disclose potential security issues by email to the project [committers](#/AUTHORS.md) as well as the [listed owners within NPM](https://docs.npmjs.com/cli/owner). We will provide an initial assessment of security reports within 48 hours and should apply patches within 2 weeks (also, feel free to contribute a fix for the issue).

View file

@ -1,152 +0,0 @@
## The `marked` function
```js
marked(markdownString [,options] [,callback])
```
|Argument |Type |Notes |
|:---------------------|:------------|:----------------------------------------------------------------------------------------------------|
|markdownString |`string` |String of markdown source to be compiled. |
|<a href="#options">options</a>|`object`|Hash of options. Can also use `marked.setOptions`. |
|callback |`function` |Called when `markdownString` has been parsed. Can be used as second argument if no `options` present.|
### Alternative using reference
```js
// Create reference instance
const marked = require('marked');
// Set options
// `highlight` example uses `highlight.js`
marked.setOptions({
renderer: new marked.Renderer(),
highlight: function(code, language) {
const hljs = require('highlight.js');
const validLanguage = hljs.getLanguage(language) ? language : 'plaintext';
return hljs.highlight(validLanguage, code).value;
},
pedantic: false,
gfm: true,
breaks: false,
sanitize: false,
smartLists: true,
smartypants: false,
xhtml: false
});
// Compile
console.log(marked(markdownString));
```
<h2 id="options">Options</h2>
|Member |Type |Default |Since |Notes |
|:-----------|:---------|:--------|:--------|:-------------|
|baseUrl |`string` |`null` |0.3.9 |A prefix url for any relative link. |
|breaks |`boolean` |`false` |v0.2.7 |If true, add `<br>` on a single line break (copies GitHub). Requires `gfm` be `true`.|
|gfm |`boolean` |`true` |v0.2.1 |If true, use approved [GitHub Flavored Markdown (GFM) specification](https://github.github.com/gfm/).|
|headerIds |`boolean` |`true` |v0.4.0 |If true, include an `id` attribute when emitting headings (h1, h2, h3, etc).|
|headerPrefix|`string` |`''` |v0.3.0 |A string to prefix the `id` attribute when emitting headings (h1, h2, h3, etc).|
|highlight |`function`|`null` |v0.3.0 |A function to highlight code blocks, see <a href="#highlight">Asynchronous highlighting</a>.|
|langPrefix |`string` |`'language-'`|v0.3.0|A string to prefix the className in a `<code>` block. Useful for syntax highlighting.|
|mangle |`boolean` |`true` |v0.3.4 |If true, autolinked email address is escaped with HTML character references.|
|pedantic |`boolean` |`false` |v0.2.1 |If true, conform to the original `markdown.pl` as much as possible. Don't fix original markdown bugs or behavior. Turns off and overrides `gfm`.|
|renderer |`object` |`new Renderer()`|v0.3.0|An object containing functions to render tokens to HTML. See [extensibility](/#/USING_PRO.md) for more details.|
|sanitize |`boolean` |`false` |v0.2.1 |If true, sanitize the HTML passed into `markdownString` with the `sanitizer` function.<br>**Warning**: This feature is deprecated and it should NOT be used as it cannot be considered secure.<br>Instead use a sanitize library, like [DOMPurify](https://github.com/cure53/DOMPurify) (recommended), [sanitize-html](https://github.com/apostrophecms/sanitize-html) or [insane](https://github.com/bevacqua/insane) on the output HTML! |
|sanitizer |`function`|`null` |v0.3.4 |A function to sanitize the HTML passed into `markdownString`.|
|silent |`boolean` |`false` |v0.2.7 |If true, the parser does not throw any exception.|
|smartLists |`boolean` |`false` |v0.2.8 |If true, use smarter list behavior than those found in `markdown.pl`.|
|smartypants |`boolean` |`false` |v0.2.9 |If true, use "smart" typographic punctuation for things like quotes and dashes.|
|xhtml |`boolean` |`false` |v0.3.2 |If true, emit self-closing HTML tags for void elements (&lt;br/&gt;, &lt;img/&gt;, etc.) with a "/" as required by XHTML.|
<h2 id="highlight">Asynchronous highlighting</h2>
Unlike `highlight.js` the `pygmentize.js` library uses asynchronous highlighting. This example demonstrates that marked is agnostic when it comes to the highlighter you use.
```js
marked.setOptions({
highlight: function(code, lang, callback) {
require('pygmentize-bundled') ({ lang: lang, format: 'html' }, code, function (err, result) {
callback(err, result.toString());
});
}
});
console.log(marked(markdownString));
```
In both examples, `code` is a `string` representing the section of code to pass to the highlighter. In this example, `lang` is a `string` informing the highlighter what programming language to use for the `code` and `callback` is the `function` the asynchronous highlighter will call once complete.
<h2 id="workers">Workers</h2>
To prevent ReDoS attacks you can run marked on a worker and terminate it when parsing takes longer than usual.
Marked can be run in a [worker thread](https://nodejs.org/api/worker_threads.html) on a node server, or a [web worker](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API) in a browser.
### Node Worker Thread
```js
// markedWorker.js
const marked = require('marked');
const { parentPort } = require('worker_threads');
parentPort.on('message', (markdownString) => {
parentPort.postMessage(marked(markdownString));
});
```
```js
// index.js
const { Worker } = require('worker_threads');
const markedWorker = new Worker('./markedWorker.js');
const markedTimeout = setTimeout(() => {
markedWorker.terminate();
throw new Error('Marked took too long!');
}, timeoutLimit);
markedWorker.on('message', (html) => {
clearTimeout(markedTimeout);
console.log(html);
markedWorker.terminate();
});
markedWorker.postMessage(markdownString);
```
### Web Worker
> **NOTE**: Web Workers send the payload from `postMessage` in an object with the payload in a `.data` property
```js
// markedWorker.js
importScripts('path/to/marked.min.js');
onmessage = (e) => {
const markdownString = e.data
postMessage(marked(markdownString));
};
```
```js
// script.js
const markedWorker = new Worker('./markedWorker.js');
const markedTimeout = setTimeout(() => {
markedWorker.terminate();
throw new Error('Marked took too long!');
}, timeoutLimit);
markedWorker.onmessage = (e) => {
clearTimeout(markedTimeout);
const html = e.data;
console.log(html);
markedWorker.terminate();
};
markedWorker.postMessage(markdownString);
```

View file

@ -1,163 +0,0 @@
## Extending Marked
To champion the single-responsibility and open/closed principles, we have tried to make it relatively painless to extend marked. If you are looking to add custom functionality, this is the place to start.
<h2 id="renderer">The renderer</h2>
The renderer is...
**Example:** Overriding default heading token by adding an embedded anchor tag like on GitHub.
```js
// Create reference instance
const marked = require('marked');
// Get reference
const renderer = new marked.Renderer();
// Override function
renderer.heading = function (text, level) {
const escapedText = text.toLowerCase().replace(/[^\w]+/g, '-');
return `
<h${level}>
<a name="${escapedText}" class="anchor" href="#${escapedText}">
<span class="header-link"></span>
</a>
${text}
</h${level}>`;
};
// Run marked
console.log(marked('# heading+', { renderer: renderer }));
```
**Output:**
```html
<h1>
<a name="heading-" class="anchor" href="#heading-">
<span class="header-link"></span>
</a>
heading+
</h1>
```
### Block level renderer methods
- code(*string* code, *string* infostring, *boolean* escaped)
- blockquote(*string* quote)
- html(*string* html)
- heading(*string* text, *number* level, *string* raw, *Slugger* slugger)
- hr()
- list(*string* body, *boolean* ordered, *number* start)
- listitem(*string* text, *boolean* task, *boolean* checked)
- checkbox(*boolean* checked)
- paragraph(*string* text)
- table(*string* header, *string* body)
- tablerow(*string* content)
- tablecell(*string* content, *object* flags)
`slugger` has the `slug` method to create an unique id from value:
```js
slugger.slug('foo') // foo
slugger.slug('foo') // foo-1
slugger.slug('foo') // foo-2
slugger.slug('foo 1') // foo-1-1
slugger.slug('foo-1') // foo-1-2
...
```
`flags` has the following properties:
```js
{
header: true || false,
align: 'center' || 'left' || 'right'
}
```
### Inline level renderer methods
- strong(*string* text)
- em(*string* text)
- codespan(*string* code)
- br()
- del(*string* text)
- link(*string* href, *string* title, *string* text)
- image(*string* href, *string* title, *string* text)
- text(*string* text)
<h2 id="lexer">The lexer</h2>
The lexer is...
<h2 id="parser">The parser</h2>
The parser is...
***
<h2 id="extend">Access to lexer and parser</h2>
You also have direct access to the lexer and parser if you so desire.
``` js
const tokens = marked.lexer(text, options);
console.log(marked.parser(tokens, options));
```
``` js
const lexer = new marked.Lexer(options);
const tokens = lexer.lex(text);
console.log(tokens);
console.log(lexer.rules);
```
``` bash
$ node
> require('marked').lexer('> i am using marked.')
[ { type: 'blockquote_start' },
{ type: 'paragraph',
text: 'i am using marked.' },
{ type: 'blockquote_end' },
links: {} ]
```
The Lexers build an array of tokens, which will be passed to their respective
Parsers. The Parsers process each token in the token arrays,
which are removed from the array of tokens:
``` js
const marked = require('marked');
const md = `
# heading
[link][1]
[1]: #heading "heading"
`;
const tokens = marked.lexer(md);
console.log(tokens);
const html = marked.parser(tokens);
console.log(html);
console.log(tokens);
```
``` bash
[ { type: 'heading', depth: 1, text: 'heading' },
{ type: 'paragraph', text: ' [link][1]' },
{ type: 'space' },
links: { '1': { href: '#heading', title: 'heading' } } ]
<h1 id="heading">heading</h1>
<p> <a href="#heading" title="heading">link</a></p>
[ links: { '1': { href: '#heading', title: 'heading' } } ]
```

View file

@ -1,426 +0,0 @@
# Markdown is broken
I have a lot of scraps of markdown engine oddities that I've collected over the
years. What you see below is slightly messy, but it's what I've managed to
cobble together to illustrate the differences between markdown engines, and
why, if there ever is a markdown specification, it has to be absolutely
thorough. There are a lot more of these little differences I have documented
elsewhere. I know I will find them lingering on my disk one day, but until
then, I'll continue to add whatever strange nonsensical things I find.
Some of these examples may only mention a particular engine compared to marked.
However, the examples with markdown.pl could easily be swapped out for
discount, upskirt, or markdown.js, and you would very easily see even more
inconsistencies.
A lot of this was written when I was very unsatisfied with the inconsistencies
between markdown engines. Please excuse the frustration noticeable in my
writing.
## Examples of markdown's "stupid" list parsing
```
$ markdown.pl
* item1
* item2
text
^D
<ul>
<li><p>item1</p>
<ul>
<li>item2</li>
</ul>
<p><p>text</p></li>
</ul></p>
```
```
$ marked
* item1
* item2
text
^D
<ul>
<li><p>item1</p>
<ul>
<li>item2</li>
</ul>
<p>text</p>
</li>
</ul>
```
Which looks correct to you?
- - -
```
$ markdown.pl
* hello
> world
^D
<p><ul>
<li>hello</p>
<blockquote>
<p>world</li>
</ul></p>
</blockquote>
```
```
$ marked
* hello
> world
^D
<ul>
<li>hello<blockquote>
<p>world</p>
</blockquote>
</li>
</ul>
```
Again, which looks correct to you?
- - -
EXAMPLE:
```
$ markdown.pl
* hello
* world
* hi
code
^D
<ul>
<li>hello
<ul>
<li>world</li>
<li>hi
code</li>
</ul></li>
</ul>
```
The code isn't a code block even though it's after the bullet margin. I know,
lets give it two more spaces, effectively making it 8 spaces past the bullet.
```
$ markdown.pl
* hello
* world
* hi
code
^D
<ul>
<li>hello
<ul>
<li>world</li>
<li>hi
code</li>
</ul></li>
</ul>
```
And, it's still not a code block. Did you also notice that the 3rd item isn't
even its own list? Markdown screws that up too because of its indentation
unaware parsing.
- - -
Let's look at some more examples of markdown's list parsing:
```
$ markdown.pl
* item1
* item2
text
^D
<ul>
<li><p>item1</p>
<ul>
<li>item2</li>
</ul>
<p><p>text</p></li>
</ul></p>
```
Misnested tags.
```
$ marked
* item1
* item2
text
^D
<ul>
<li><p>item1</p>
<ul>
<li>item2</li>
</ul>
<p>text</p>
</li>
</ul>
```
Which looks correct to you?
- - -
```
$ markdown.pl
* hello
> world
^D
<p><ul>
<li>hello</p>
<blockquote>
<p>world</li>
</ul></p>
</blockquote>
```
More misnested tags.
```
$ marked
* hello
> world
^D
<ul>
<li>hello<blockquote>
<p>world</p>
</blockquote>
</li>
</ul>
```
Again, which looks correct to you?
- - -
# Why quality matters - Part 2
``` bash
$ markdown.pl
* hello
> world
^D
<p><ul>
<li>hello</p>
<blockquote>
<p>world</li>
</ul></p>
</blockquote>
```
``` bash
$ sundown # upskirt
* hello
> world
^D
<ul>
<li>hello
&gt; world</li>
</ul>
```
``` bash
$ marked
* hello
> world
^D
<ul><li>hello <blockquote><p>world</p></blockquote></li></ul>
```
Which looks correct to you?
- - -
See: https://github.com/evilstreak/markdown-js/issues/23
``` bash
$ markdown.pl # upskirt/markdown.js/discount
* hello
var a = 1;
* world
^D
<ul>
<li>hello
var a = 1;</li>
<li>world</li>
</ul>
```
``` bash
$ marked
* hello
var a = 1;
* world
^D
<ul><li>hello
<pre>code>var a = 1;</code></pre></li>
<li>world</li></ul>
```
Which looks more reasonable? Why shouldn't code blocks be able to appear in
list items in a sane way?
- - -
``` bash
$ markdown.js
<div>hello</div>
<span>hello</span>
^D
<p>&lt;div&gt;hello&lt;/div&gt;</p>
<p>&lt;span&gt;hello&lt;/span&gt;</p>
```
``` bash
$ marked
<div>hello</div>
<span>hello</span>
^D
<div>hello</div>
<p><span>hello</span>
</p>
```
- - -
See: https://github.com/evilstreak/markdown-js/issues/27
``` bash
$ markdown.js
[![an image](/image)](/link)
^D
<p><a href="/image)](/link">![an image</a></p>
```
``` bash
$ marked
[![an image](/image)](/link)
^D
<p><a href="/link"><img src="/image" alt="an image"></a>
</p>
```
- - -
See: https://github.com/evilstreak/markdown-js/issues/24
``` bash
$ markdown.js
> a
> b
> c
^D
<blockquote><p>a</p><p>bundefined&gt; c</p></blockquote>
```
``` bash
$ marked
> a
> b
> c
^D
<blockquote><p>a
</p></blockquote>
<blockquote><p>b
</p></blockquote>
<blockquote><p>c
</p></blockquote>
```
- - -
``` bash
$ markdown.pl
* hello
* world
how
are
you
* today
* hi
^D
<ul>
<li><p>hello</p>
<ul>
<li>world
how</li>
</ul>
<p>are
you</p>
<ul>
<li>today</li>
</ul></li>
<li>hi</li>
</ul>
```
``` bash
$ marked
* hello
* world
how
are
you
* today
* hi
^D
<ul>
<li><p>hello</p>
<ul>
<li><p>world
how</p>
<p>are
you</p>
</li>
<li><p>today</p>
</li>
</ul>
</li>
<li>hi</li>
</ul>
```

View file

@ -1,72 +0,0 @@
html, body {
margin: 0;
padding: 0;
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
color: #333;
background-color: #fbfbfb;
height: 100%;
}
textarea {
font-family: Menlo, Monaco, Consolas, "Courier New", monospace;
font-size: 12px;
resize: none;
}
header {
padding-top: 10px;
display: flex;
height: 58px;
}
header h1 {
margin: 0;
}
.github-ribbon {
position: absolute;
top: 0;
right: 0;
border: 0;
z-index: 1000;
}
.containers {
display: flex;
height: calc(100vh - 68px);
}
.container {
flex-basis: 50%;
padding: 5px;
display: flex;
flex-direction: column;
height: 100%;
box-sizing: border-box;
}
.pane, .inputPane {
margin-top: 5px;
padding: 0.6em;
border: 1px solid #ccc;
overflow: auto;
flex-grow: 1;
flex-shrink: 1;
}
#preview {
display: flex;
}
#preview iframe {
flex-grow: 1;
}
#main {
display: none;
}
.error {
border-color: red;
background-color: #FEE
}

View file

@ -1,534 +0,0 @@
/* globals marked, unfetch, ES6Promise, Promise */ // eslint-disable-line no-redeclare
if (!window.Promise) {
window.Promise = ES6Promise;
}
if (!window.fetch) {
window.fetch = unfetch;
}
onunhandledrejection = function(e) {
throw e.reason;
};
var $loadingElem = document.querySelector('#loading');
var $mainElem = document.querySelector('#main');
var $markdownElem = document.querySelector('#markdown');
var $markedVerElem = document.querySelector('#markedVersion');
var $commitVerElem = document.querySelector('#commitVersion');
var $markedVer = document.querySelector('#markedCdn');
var $optionsElem = document.querySelector('#options');
var $outputTypeElem = document.querySelector('#outputType');
var $inputTypeElem = document.querySelector('#inputType');
var $responseTimeElem = document.querySelector('#responseTime');
var $previewElem = document.querySelector('#preview');
var $previewIframe = document.querySelector('#preview iframe');
var $permalinkElem = document.querySelector('#permalink');
var $clearElem = document.querySelector('#clear');
var $htmlElem = document.querySelector('#html');
var $lexerElem = document.querySelector('#lexer');
var $panes = document.querySelectorAll('.pane');
var $inputPanes = document.querySelectorAll('.inputPane');
var lastInput = '';
var inputDirty = true;
var $activeOutputElem = null;
var search = searchToObject();
var markedVersions = {
master: 'https://cdn.jsdelivr.net/gh/markedjs/marked/lib/marked.js'
};
var markedVersionCache = {};
var delayTime = 1;
var checkChangeTimeout = null;
var markedWorker;
$previewIframe.addEventListener('load', handleIframeLoad);
$outputTypeElem.addEventListener('change', handleOutputChange, false);
$inputTypeElem.addEventListener('change', handleInputChange, false);
$markedVerElem.addEventListener('change', handleVersionChange, false);
$markdownElem.addEventListener('change', handleInput, false);
$markdownElem.addEventListener('keyup', handleInput, false);
$markdownElem.addEventListener('keypress', handleInput, false);
$markdownElem.addEventListener('keydown', handleInput, false);
$optionsElem.addEventListener('change', handleInput, false);
$optionsElem.addEventListener('keyup', handleInput, false);
$optionsElem.addEventListener('keypress', handleInput, false);
$optionsElem.addEventListener('keydown', handleInput, false);
$commitVerElem.style.display = 'none';
$commitVerElem.addEventListener('keypress', handleAddVersion, false);
$clearElem.addEventListener('click', handleClearClick, false);
Promise.all([
setInitialQuickref(),
setInitialOutputType(),
setInitialText(),
setInitialVersion()
.then(setInitialOptions)
]).then(function() {
handleInputChange();
handleOutputChange();
checkForChanges();
setScrollPercent(0);
$loadingElem.style.display = 'none';
$mainElem.style.display = 'block';
});
function setInitialText() {
if ('text' in search) {
$markdownElem.value = search.text;
} else {
return fetch('./initial.md')
.then(function(res) { return res.text(); })
.then(function(text) {
if ($markdownElem.value === '') {
$markdownElem.value = text;
}
});
}
}
function setInitialQuickref() {
return fetch('./quickref.md')
.then(function(res) { return res.text(); })
.then(function(text) {
document.querySelector('#quickref').value = text;
});
}
function setInitialVersion() {
return fetch('https://data.jsdelivr.com/v1/package/npm/marked')
.then(function(res) {
return res.json();
})
.then(function(json) {
for (var i = 0; i < json.versions.length; i++) {
var ver = json.versions[i];
markedVersions[ver] = 'https://cdn.jsdelivr.net/npm/marked@' + ver + '/lib/marked.js';
var opt = document.createElement('option');
opt.textContent = ver;
opt.value = ver;
$markedVerElem.appendChild(opt);
}
})
.then(function() {
return fetch('https://api.github.com/repos/markedjs/marked/commits')
.then(function(res) {
return res.json();
})
.then(function(json) {
markedVersions.master = 'https://cdn.jsdelivr.net/gh/markedjs/marked@' + json[0].sha + '/lib/marked.js';
})
.catch(function() {
// do nothing
// uses url without commit
});
})
.then(function() {
if (search.version) {
if (markedVersions[search.version]) {
return search.version;
} else {
var match = search.version.match(/^(\w+):(.+)$/);
if (match) {
switch (match[1]) {
case 'commit':
addCommitVersion(search.version, match[2].substring(0, 7), match[2]);
return search.version;
case 'pr':
return getPrCommit(match[2])
.then(function(commit) {
if (!commit) {
return 'master';
}
addCommitVersion(search.version, 'PR #' + match[2], commit);
return search.version;
});
}
}
}
}
return 'master';
})
.then(function(version) {
$markedVerElem.value = version;
})
.then(updateVersion);
}
function setInitialOptions() {
if ('options' in search) {
$optionsElem.value = search.options;
} else {
setDefaultOptions();
}
}
function setInitialOutputType() {
if (search.outputType) {
$outputTypeElem.value = search.outputType;
}
}
function handleIframeLoad() {
lastInput = '';
inputDirty = true;
}
function handleInput() {
inputDirty = true;
};
function handleVersionChange() {
if ($markedVerElem.value === 'commit' || $markedVerElem.value === 'pr') {
$commitVerElem.style.display = '';
} else {
$commitVerElem.style.display = 'none';
updateVersion();
}
}
function handleClearClick() {
$markdownElem.value = '';
$markedVerElem.value = 'master';
$commitVerElem.style.display = 'none';
updateVersion().then(setDefaultOptions);
}
function handleAddVersion(e) {
if (e.which === 13) {
switch ($markedVerElem.value) {
case 'commit':
var commit = $commitVerElem.value.toLowerCase();
if (!commit.match(/^[0-9a-f]{40}$/)) {
alert('That is not a valid commit');
return;
}
addCommitVersion('commit:' + commit, commit.substring(0, 7), commit);
$markedVerElem.value = 'commit:' + commit;
$commitVerElem.style.display = 'none';
$commitVerElem.value = '';
updateVersion();
break;
case 'pr':
$commitVerElem.disabled = true;
var pr = $commitVerElem.value.replace(/\D/g, '');
getPrCommit(pr)
.then(function(commit) {
$commitVerElem.disabled = false;
if (!commit) {
alert('That is not a valid PR');
return;
}
addCommitVersion('pr:' + pr, 'PR #' + pr, commit);
$markedVerElem.value = 'pr:' + pr;
$commitVerElem.style.display = 'none';
$commitVerElem.value = '';
updateVersion();
});
}
}
}
function handleInputChange() {
handleChange($inputPanes, $inputTypeElem.value);
}
function handleOutputChange() {
$activeOutputElem = handleChange($panes, $outputTypeElem.value);
updateLink();
}
function handleChange(panes, visiblePane) {
var active = null;
for (var i = 0; i < panes.length; i++) {
if (panes[i].id === visiblePane) {
panes[i].style.display = '';
active = panes[i];
} else {
panes[i].style.display = 'none';
}
}
return active;
};
function addCommitVersion(value, text, commit) {
if (markedVersions[value]) {
return;
}
markedVersions[value] = 'https://cdn.jsdelivr.net/gh/markedjs/marked@' + commit + '/lib/marked.js';
var opt = document.createElement('option');
opt.textContent = text;
opt.value = value;
$markedVerElem.insertBefore(opt, $markedVerElem.firstChild);
}
function getPrCommit(pr) {
return fetch('https://api.github.com/repos/markedjs/marked/pulls/' + pr + '/commits')
.then(function(res) {
return res.json();
})
.then(function(json) {
return json[json.length - 1].sha;
}).catch(function() {
// return undefined
});
}
function setDefaultOptions() {
if (window.Worker) {
messageWorker({
task: 'defaults',
version: markedVersions[$markedVerElem.value]
});
} else {
var defaults = marked.getDefaults();
setOptions(defaults);
}
}
function setOptions(opts) {
$optionsElem.value = JSON.stringify(
opts,
function(key, value) {
if (value && typeof value === 'object' && Object.getPrototypeOf(value) !== Object.prototype) {
return undefined;
}
return value;
}, ' ');
}
function searchToObject() {
// modified from https://stackoverflow.com/a/7090123/806777
var pairs = location.search.slice(1).split('&');
var obj = {};
for (var i = 0; i < pairs.length; i++) {
if (pairs[i] === '') {
continue;
}
var pair = pairs[i].split('=');
obj[decodeURIComponent(pair.shift())] = decodeURIComponent(pair.join('='));
}
return obj;
}
function jsonString(input) {
var output = (input + '')
.replace(/\n/g, '\\n')
.replace(/\r/g, '\\r')
.replace(/\t/g, '\\t')
.replace(/\f/g, '\\f')
.replace(/[\\"']/g, '\\$&')
.replace(/\u0000/g, '\\0');
return '"' + output + '"';
};
function getScrollSize() {
var e = $activeOutputElem;
return e.scrollHeight - e.clientHeight;
};
function getScrollPercent() {
var size = getScrollSize();
if (size <= 0) {
return 1;
}
return $activeOutputElem.scrollTop / size;
};
function setScrollPercent(percent) {
$activeOutputElem.scrollTop = percent * getScrollSize();
};
function updateLink() {
var outputType = '';
if ($outputTypeElem.value !== 'preview') {
outputType = 'outputType=' + $outputTypeElem.value + '&';
}
$permalinkElem.href = '?' + outputType + 'text=' + encodeURIComponent($markdownElem.value)
+ '&options=' + encodeURIComponent($optionsElem.value)
+ '&version=' + encodeURIComponent($markedVerElem.value);
history.replaceState('', document.title, $permalinkElem.href);
}
function updateVersion() {
if (window.Worker) {
handleInput();
return Promise.resolve();
}
var promise;
if (markedVersionCache[$markedVerElem.value]) {
promise = Promise.resolve(markedVersionCache[$markedVerElem.value]);
} else {
promise = fetch(markedVersions[$markedVerElem.value])
.then(function(res) { return res.text(); })
.then(function(text) {
markedVersionCache[$markedVerElem.value] = text;
return text;
});
}
return promise.then(function(text) {
var script = document.createElement('script');
script.textContent = text;
$markedVer.parentNode.replaceChild(script, $markedVer);
$markedVer = script;
}).then(handleInput);
}
function checkForChanges() {
if (inputDirty && $markedVerElem.value !== 'commit' && $markedVerElem.value !== 'pr' && (typeof marked !== 'undefined' || window.Worker)) {
inputDirty = false;
updateLink();
var options = {};
var optionsString = $optionsElem.value || '{}';
try {
var newOptions = JSON.parse(optionsString);
options = newOptions;
$optionsElem.classList.remove('error');
} catch (err) {
$optionsElem.classList.add('error');
}
var version = markedVersions[$markedVerElem.value];
var markdown = $markdownElem.value;
var hash = version + markdown + optionsString;
if (lastInput !== hash) {
lastInput = hash;
if (window.Worker) {
delayTime = 100;
messageWorker({
task: 'parse',
version: version,
markdown: markdown,
options: options
});
} else {
var startTime = new Date();
var lexed = marked.lexer(markdown, options);
var lexedList = [];
for (var i = 0; i < lexed.length; i++) {
var lexedLine = [];
for (var j in lexed[i]) {
lexedLine.push(j + ':' + jsonString(lexed[i][j]));
}
lexedList.push('{' + lexedLine.join(', ') + '}');
}
var parsed = marked.parser(lexed, options);
var scrollPercent = getScrollPercent();
setParsed(parsed, lexedList.join('\n'));
setScrollPercent(scrollPercent);
var endTime = new Date();
delayTime = endTime - startTime;
setResponseTime(delayTime);
if (delayTime < 50) {
delayTime = 50;
} else if (delayTime > 500) {
delayTime = 1000;
}
}
}
}
checkChangeTimeout = window.setTimeout(checkForChanges, delayTime);
};
function setResponseTime(ms) {
var amount = ms;
var suffix = 'ms';
if (ms > 1000 * 60 * 60) {
amount = 'Too Long';
suffix = '';
} else if (ms > 1000 * 60) {
amount = '>' + Math.floor(ms / (1000 * 60));
suffix = 'm';
} else if (ms > 1000) {
amount = '>' + Math.floor(ms / 1000);
suffix = 's';
}
$responseTimeElem.textContent = amount + suffix;
}
function setParsed(parsed, lexed) {
try {
$previewIframe.contentDocument.body.innerHTML = parsed;
} catch (ex) {}
$htmlElem.value = parsed;
$lexerElem.value = lexed;
}
function messageWorker(message) {
if (!markedWorker || markedWorker.working) {
if (markedWorker) {
clearTimeout(markedWorker.timeout);
markedWorker.terminate();
}
markedWorker = new Worker('worker.js');
markedWorker.onmessage = function(e) {
clearTimeout(markedWorker.timeout);
markedWorker.working = false;
switch (e.data.task) {
case 'defaults':
setOptions(e.data.defaults);
break;
case 'parse':
$previewElem.classList.remove('error');
$htmlElem.classList.remove('error');
$lexerElem.classList.remove('error');
var scrollPercent = getScrollPercent();
setParsed(e.data.parsed, e.data.lexed);
setScrollPercent(scrollPercent);
setResponseTime(e.data.time);
break;
}
clearTimeout(checkChangeTimeout);
delayTime = 10;
checkForChanges();
};
markedWorker.onerror = markedWorker.onmessageerror = function(err) {
clearTimeout(markedWorker.timeout);
var error = 'There was an error in the Worker';
if (err) {
if (err.message) {
error = err.message;
} else {
error = err;
}
}
error = error.replace(/^Uncaught Error: /, '');
$previewElem.classList.add('error');
$htmlElem.classList.add('error');
$lexerElem.classList.add('error');
setParsed(error, error);
setScrollPercent(0);
};
}
if (message.task !== 'defaults') {
markedWorker.working = true;
workerTimeout(0);
}
markedWorker.postMessage(message);
}
function workerTimeout(seconds) {
markedWorker.timeout = setTimeout(function() {
seconds++;
markedWorker.onerror('Marked has taken longer than ' + seconds + ' second' + (seconds > 1 ? 's' : '') + ' to respond...');
workerTimeout(seconds);
}, 1000);
}

View file

@ -1,78 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<title>Marked Demo</title>
<link rel="stylesheet" href="./demo.css" type="text/css" />
</head>
<body>
<a href="https://github.com/markedjs/marked">
<img class="github-ribbon" src="https://s3.amazonaws.com/github/ribbons/forkme_right_darkblue_121621.png" alt="Fork me on GitHub">
</a>
<header>
<a href="../">
<img src="../img/logo-black.svg" height="64px" width="64px" />
</a>
<h1>Marked Demo</h1>
</header>
<div id="loading">Loading...</div>
<div id="main">
<div class="containers">
<div class="container">
<div class="label">
<span>Input</span> ·
<a id="permalink">Permalink</a> ·
<span>Version: </span>
<select id="markedVersion">
<option value="pr">Add PR:</option>
<option value="commit">Add Commit:</option>
<option value="master" selected>master</option>
</select>
<input type="text" id="commitVersion" title="Press ENTER to add commit" />·
<button id="clear">Clear</button>
<select id="inputType">
<option value="markdown">Markdown</option>
<option value="options">Options</option>
</select>
</div>
<textarea id="markdown" class="inputPane"></textarea>
<textarea id="options" class="inputPane" placeholder="Options (as JSON)"></textarea>
</div>
<div class="container">
<div class="label">
<select id="outputType">
<option value="preview">Preview</option>
<option value="html">HTML Source</option>
<option value="lexer">Lexer Data</option>
<option value="quickref">Quick Reference</option>
</select> ·
Response Time:
<span id="responseTime"></span>
</div>
<div id="preview" class="pane">
<noscript>
<h2>You'll need to enable Javascript to use this tool.</h2>
</noscript>
<iframe src="./preview.html" frameborder="0" sandbox="allow-same-origin allow-top-navigation-by-user-activation"></iframe>
</div>
<textarea id="html" class="pane" readonly="readonly"></textarea>
<textarea id="lexer" class="pane" readonly="readonly"></textarea>
<textarea id="quickref" class="pane" readonly="readonly"></textarea>
</div>
</div>
</div>
<script id="markedCdn"></script>
<script src="https://cdn.jsdelivr.net/npm/es6-promise/dist/es6-promise.js"></script>
<script src="https://cdn.jsdelivr.net/npm/unfetch/dist/unfetch.umd.js"></script>
<script src="./demo.js"></script>
</body>
</html>

View file

@ -1,36 +0,0 @@
Marked - Markdown Parser
========================
[Marked] lets you convert [Markdown] into HTML. Markdown is a simple text format whose goal is to be very easy to read and write, even when not converted to HTML. This demo page will let you type anything you like and see how it gets converted. Live. No more waiting around.
How To Use The Demo
-------------------
1. Type in stuff on the left.
2. See the live updates on the right.
That's it. Pretty simple. There's also a drop-down option in the upper right to switch between various views:
- **Preview:** A live display of the generated HTML as it would render in a browser.
- **HTML Source:** The generated HTML before your browser makes it pretty.
- **Lexer Data:** What [marked] uses internally, in case you like gory stuff like this.
- **Quick Reference:** A brief run-down of how to format things using markdown.
Why Markdown?
-------------
It's easy. It's not overly bloated, unlike HTML. Also, as the creator of [markdown] says,
> The overriding design goal for Markdown's
> formatting syntax is to make it as readable
> as possible. The idea is that a
> Markdown-formatted document should be
> publishable as-is, as plain text, without
> looking like it's been marked up with tags
> or formatting instructions.
Ready to start writing? Either start changing stuff on the left or
[clear everything](/demo/?text=) with a simple click.
[Marked]: https://github.com/markedjs/marked/
[Markdown]: http://daringfireball.net/projects/markdown/

View file

@ -1,12 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>marked.js preview</title>
<link rel="stylesheet" href="./demo.css" />
<base target="_parent">
</head>
<body>
</body>
</html>

View file

@ -1,167 +0,0 @@
Markdown Quick Reference
========================
This guide is a very brief overview, with examples, of the syntax that [Markdown] supports. It is itself written in Markdown and you can copy the samples over to the left-hand pane for experimentation. It's shown as *text* and not *rendered HTML*.
[Markdown]: http://daringfireball.net/projects/markdown/
Simple Text Formatting
======================
First thing is first. You can use *stars* or _underscores_ for italics. **Double stars** and __double underscores__ do bold. ***Three together*** do ___both___.
Paragraphs are pretty easy too. Just have a blank line between chunks of text.
> This chunk of text is in a block quote. Its multiple lines will all be
> indended a bit from the rest of the text.
>
> > Multiple levels of block quotes also work.
Sometimes you want to include some code, such as when you are explaining how `<h1>` HTML tags work, or maybe you are a programmer and you are discussing `someMethod()`.
If you want to include some code and have
newlines preserved, indent the line with a tab
or at least four spaces.
Extra spaces work here too.
This is also called preformatted text and it is useful for showing examples.
The text will stay as text, so any *markdown* or <u>HTML</u> you add will
not show up formatted. This way you can show markdown examples in a
markdown document.
> You can also use preformatted text with your blockquotes
> as long as you add at least five spaces.
Headings
========
There are a couple of ways to make headings. Using three or more equals signs on a line under a heading makes it into an "h1" style. Three or more hyphens under a line makes it "h2" (slightly smaller). You can also use multiple pound symbols before and after a heading. Pounds after the title are ignored. Here's some examples:
This is H1
==========
This is H2
----------
# This is H1
## This is H2
### This is H3 with some extra pounds ###
#### You get the idea ####
##### I don't need extra pounds at the end
###### H6 is the max
Links
=====
Let's link to a few sites. First, let's use the bare URL, like <http://www.github.com>. Great for text, but ugly for HTML.
Next is an inline link to [Google](http://www.google.com). A little nicer.
This is a reference-style link to [Wikipedia] [1].
Lastly, here's a pretty link to [Yahoo]. The reference-style and pretty links both automatically use the links defined below, but they could be defined *anywhere* in the markdown and are removed from the HTML. The names are also case insensitive, so you can use [YaHoO] and have it link properly.
[1]: http://www.wikipedia.org/
[Yahoo]: http://www.yahoo.com/
Title attributes may be added to links by adding text after a link.
This is the [inline link](http://www.bing.com "Bing") with a "Bing" title.
You can also go to [W3C] [2] and maybe visit a [friend].
[2]: http://w3c.org (The W3C puts out specs for web-based things)
[Friend]: http://facebook.com/ "Facebook!"
Email addresses in plain text are not linked: test@example.com.
Email addresses wrapped in angle brackets are linked: <test@example.com>.
They are also obfuscated so that email harvesting spam robots hopefully won't get them.
Lists
=====
* This is a bulleted list
* Great for shopping lists
- You can also use hyphens
+ Or plus symbols
The above is an "unordered" list. Now, on for a bit of order.
1. Numbered lists are also easy
2. Just start with a number
3738762. However, the actual number doesn't matter when converted to HTML.
1. This will still show up as 4.
You might want a few advanced lists:
- This top-level list is wrapped in paragraph tags
- This generates an extra space between each top-level item.
- You do it by adding a blank line
- This nested list also has blank lines between the list items.
- How to create nested lists
1. Start your regular list
2. Indent nested lists with four spaces
3. Further nesting means you should indent with four more spaces
* This line is indented with eight spaces.
- List items can be quite lengthy. You can keep typing and either continue
them on the next line with no indentation.
- Alternately, if that looks ugly, you can also
indent the next line a bit for a prettier look.
- You can put large blocks of text in your list by just indenting with four spaces.
This is formatted the same as code, but you can inspect the HTML
and find that it's just wrapped in a `<p>` tag and *won't* be shown
as preformatted text.
You can keep adding more and more paragraphs to a single
list item by adding the traditional blank line and then keep
on indenting the paragraphs with four spaces. You really need
to only indent the first line, but that looks ugly.
- Lists support blockquotes
> Just like this example here. By the way, you can
> nest lists inside blockquotes!
> - Fantastic!
- Lists support preformatted text
You just need to indent eight spaces.
Even More
=========
Horizontal Rule
---------------
If you need a horizontal rule you just need to put at least three hyphens, asterisks, or underscores on a line by themselves. You can also even put spaces between the characters.
---
****************************
_ _ _ _ _ _ _
Those three all produced horizontal lines. Keep in mind that three hyphens under any text turns that text into a heading, so add a blank like if you use hyphens.
Images
------
Images work exactly like links, but they have exclamation points in front. They work with references and titles too.
![Google Logo](http://www.google.com/images/errors/logo_sm.gif) and ![Happy].
[Happy]: http://www.wpclipart.com/smiley/simple_smiley/smiley_face_simple_green_small.png ("Smiley face")
Inline HTML
-----------
If markdown is too limiting, you can just insert your own <strike>crazy</strike> HTML. Span-level HTML <u>can *still* use markdown</u>. Block level elements must be separated from text by a blank line and must not have any spaces before the opening and closing HTML.
<div style='font-family: "Comic Sans", sans-serif;'>
It is a pity, but markdown does **not** work in here for most markdown parsers. [Marked] handles it pretty well.
</div>

View file

@ -1,105 +0,0 @@
/* globals marked, unfetch, ES6Promise, Promise */ // eslint-disable-line no-redeclare
if (!self.Promise) {
self.importScripts('https://cdn.jsdelivr.net/npm/es6-promise/dist/es6-promise.js');
self.Promise = ES6Promise;
}
if (!self.fetch) {
self.importScripts('https://cdn.jsdelivr.net/npm/unfetch/dist/unfetch.umd.js');
self.fetch = unfetch;
}
var versionCache = {};
var currentVersion;
onunhandledrejection = function(e) {
throw e.reason;
};
onmessage = function(e) {
if (e.data.version === currentVersion) {
parse(e);
} else {
loadVersion(e.data.version).then(function() {
parse(e);
});
}
};
function parse(e) {
switch (e.data.task) {
case 'defaults':
var defaults = {};
if (typeof marked.getDefaults === 'function') {
defaults = marked.getDefaults();
delete defaults.renderer;
} else if ('defaults' in marked) {
for (var prop in marked.defaults) {
if (prop !== 'renderer') {
defaults[prop] = marked.defaults[prop];
}
}
}
postMessage({
task: e.data.task,
defaults: defaults
});
break;
case 'parse':
var startTime = new Date();
var lexed = marked.lexer(e.data.markdown, e.data.options);
var lexedList = [];
for (var i = 0; i < lexed.length; i++) {
var lexedLine = [];
for (var j in lexed[i]) {
lexedLine.push(j + ':' + jsonString(lexed[i][j]));
}
lexedList.push('{' + lexedLine.join(', ') + '}');
}
var parsed = marked.parser(lexed, e.data.options);
var endTime = new Date();
// setTimeout(function () {
postMessage({
task: e.data.task,
lexed: lexedList.join('\n'),
parsed: parsed,
time: endTime - startTime
});
// }, 10000);
break;
}
}
function jsonString(input) {
var output = (input + '')
.replace(/\n/g, '\\n')
.replace(/\r/g, '\\r')
.replace(/\t/g, '\\t')
.replace(/\f/g, '\\f')
.replace(/[\\"']/g, '\\$&')
.replace(/\u0000/g, '\\0');
return '"' + output + '"';
};
function loadVersion(ver) {
var promise;
if (versionCache[ver]) {
promise = Promise.resolve(versionCache[ver]);
} else {
promise = fetch(ver)
.then(function(res) { return res.text(); })
.then(function(text) {
versionCache[ver] = text;
return text;
});
}
return promise.then(function(text) {
try {
// eslint-disable-next-line no-new-func
Function(text)();
} catch (err) {
throw new Error('Cannot load that version of marked');
}
currentVersion = ver;
});
}

View file

@ -1,133 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 16.0.4, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
width="996px" height="439.589px" viewBox="0 0 996 439.589" enable-background="new 0 0 996 439.589" xml:space="preserve">
<g>
<g id="Marked_Letters">
<path fill="#010101" d="M185.044,358.869h-9.961l-1.143-25.986l-14.092,25.986h-6.24l-6.709-26.807l-8.818,26.807h-10.225
l13.389-40.605h10.928l6.416,25.781l13.916-25.781h10.635L185.044,358.869z"/>
<path fill="#010101" d="M226.617,358.869h-10.811l-1.084-8.994H202.33l-4.102,8.994h-10.605l20.566-40.605h10.986L226.617,358.869
z M214.019,344.162l-1.934-15.527l-7.061,15.527H214.019z"/>
<path fill="#010101" d="M267.712,358.869h-11.338l-4.043-8.467c-1.191-2.559-2.227-4.478-3.105-5.757s-1.651-2.07-2.314-2.373
c-0.664-0.303-1.553-0.454-2.666-0.454h-1.553l-2.578,17.051h-10.283l5.889-40.605h15.85c5.761,0,9.672,1.27,11.733,3.809
c2.061,2.539,3.091,5.283,3.091,8.232c0,3.105-0.854,5.522-2.563,7.251c-1.709,1.729-4.097,2.798-7.163,3.208
c1.562,0.898,2.769,1.953,3.618,3.164s1.86,3.096,3.032,5.654L267.712,358.869z M255.525,330.012c0-1.562-0.571-2.925-1.714-4.087
s-3.453-1.743-6.929-1.743h-1.611l-1.699,11.719h4.102c2.89,0,4.922-0.61,6.094-1.831S255.525,331.496,255.525,330.012z"/>
<path fill="#010101" d="M313.705,318.264l-17.578,18.223l14.912,22.383h-11.807l-14.59-21.65l-3.282,21.65h-10.342l6.064-40.605
h10.401l-2.578,17.256l16.611-17.256H313.705z"/>
<path fill="#010101" d="M346.426,318.264L345.4,325.5h-16.436l-1.26,8.818h14.268l-1.025,7.266h-14.385l-1.377,10.02h19.98
l-1.025,7.266h-30.439l5.889-40.605H346.426z"/>
<path fill="#010101" d="M389.953,338.156c0,4.512-0.86,8.301-2.578,11.367c-1.719,3.066-4.273,5.391-7.662,6.973
s-7.583,2.373-12.582,2.373H348.41l5.83-40.605h14.941c3.534,0,6.625,0.527,9.271,1.582s4.82,2.51,6.52,4.365
s2.953,3.97,3.764,6.343S389.953,335.461,389.953,338.156z M379.377,338.303c0-2.031-0.387-4.004-1.158-5.918
s-2.192-3.54-4.262-4.878c-2.07-1.338-4.941-2.007-8.613-2.007h-1.67l-3.779,26.104h5.449c2.597,0,4.828-0.391,6.693-1.172
s3.33-1.821,4.395-3.12s1.821-2.71,2.271-4.233C379.152,341.555,379.377,339.963,379.377,338.303z"/>
</g>
<g>
<path fill="#010101" d="M406.922,307.149l-3.703,26.35c-0.609,4.324-2.72,6.486-6.33,6.486c-0.773,0-1.553-0.07-2.338-0.211
l-0.756-3.023c0.879,0.188,1.67,0.281,2.373,0.281c1.219,0,2.118-0.302,2.698-0.905s0.964-1.55,1.151-2.839l3.674-26.139H406.922z
"/>
<path fill="#010101" d="M423.027,307.8l-0.475,3.357c-1.312-0.867-2.748-1.301-4.307-1.301c-1.312,0-2.361,0.34-3.146,1.02
s-1.178,1.576-1.178,2.689c0,0.551,0.188,1.131,0.562,1.74s1.471,1.644,3.287,3.103s3.015,2.739,3.595,3.841
s0.87,2.203,0.87,3.305c0,1.758-0.662,3.275-1.986,4.553s-3.088,1.916-5.291,1.916c-1.957,0-3.938-0.586-5.941-1.758l0.51-3.604
c2.238,1.559,4.16,2.338,5.766,2.338c0.867,0,1.69-0.284,2.47-0.852c0.779-0.568,1.169-1.339,1.169-2.312
c0-0.62-0.197-1.265-0.59-1.933c-0.394-0.667-1.502-1.745-3.324-3.232c-1.824-1.486-3.014-2.778-3.568-3.874
c-0.555-1.097-0.832-2.185-0.832-3.263c0-1.899,0.785-3.491,2.355-4.775c1.57-1.283,3.422-1.926,5.555-1.926
C420.016,306.833,421.516,307.155,423.027,307.8z"/>
</g>
<g>
<g>
<path fill="#010101" d="M357,200v-92c0-8.284-6.716-15-15-15H172c-8.284,0-15,6.716-15,15v92H357z"/>
<path fill="#010101" d="M157,205v73c0,8.284,6.716,15,15,15h170c8.284,0,15-6.716,15-15v-73H157z"/>
</g>
<g>
<path fill="#FFFFFF" d="M208.737,180.15v-33.125l16.987,21.234l16.987-21.234v33.125h16.987v-57.756h-16.987l-16.987,21.234
l-16.987-21.234H191.75v57.756H208.737z"/>
<path fill="#FFFFFF" d="M322.55,151.272h-16.987v-28.878h-16.986v28.878h-16.987L297.069,181L322.55,151.272z"/>
</g>
<g>
<path fill="#FFFFFF" d="M217,241.154c0,2.124-1.722,3.846-3.845,3.846h-5.31c-2.124,0-3.845-1.722-3.845-3.846v-0.309
c0-2.124,1.722-3.846,3.845-3.846h5.31c2.124,0,3.845,1.722,3.845,3.846V241.154z"/>
<path fill="#FFFFFF" d="M207.859,256.5c0,1.933-1.1,3.5-2.457,3.5h-12.046c-1.357,0-2.457-1.567-2.457-3.5l0,0
c0-1.933,1.1-3.5,2.457-3.5h12.046C206.759,253,207.859,254.567,207.859,256.5L207.859,256.5z"/>
<path fill="#FFFFFF" d="M328.044,238.021c-1.058-3.094-2.692-5.85-4.907-8.269c-2.215-2.418-5.049-4.314-8.498-5.689
c-3.45-1.375-7.479-2.063-12.086-2.063h-19.477l-7.6,52.932h24.403c6.518,0,11.984-1.032,16.402-3.094
c4.416-2.062,7.746-5.092,9.986-9.09c2.24-3.996,3.361-8.936,3.361-14.817C329.629,244.418,329.1,241.115,328.044,238.021z
M314.964,254.347c-0.586,1.986-1.573,3.826-2.96,5.519c-1.389,1.694-3.297,3.049-5.729,4.067s-5.34,1.528-8.727,1.528h-7.103
l4.927-34.027h2.176c4.787,0,8.529,0.872,11.229,2.616c2.698,1.744,4.55,3.863,5.557,6.358c1.005,2.494,1.508,5.066,1.508,7.713
C315.842,250.286,315.549,252.361,314.964,254.347z"/>
<path fill="#FFFFFF" d="M246.148,241.043l1.373-9.61h21.424l1.336-9.434h-10.601h-24.38h-1.256c-2.125,0-3.846,1.877-3.846,4
s1.721,4,3.846,4h0.096h3.793c1.928,0,3.491,1.563,3.491,3.491c0,1.928-1.563,3.491-3.491,3.491h-4.806L233.126,237h-8.983
c-1.299,0-2.353,1.786-2.353,3.988c0,2.203,1.053,3.989,2.353,3.989h7.826L231.966,245h3.189c2.124,0,3.845,1.722,3.845,3.846
v0.309c0,2.124-1.722,3.846-3.845,3.846h-4.349h-0.96H216.2c-1.933,0-3.5,1.567-3.5,3.5s1.567,3.5,3.5,3.5h8.645h4.945h0.364
c2.124,0,3.845,1.722,3.845,3.846v0.309c0,2.124-1.722,3.846-3.845,3.846h-1.524h-3.785h-1.8c-2.125,0-3.846,1.377-3.846,3.5
s1.721,3.5,3.846,3.5h25.636c0.273,0,0.538-0.024,0.795-0.068h17.828l1.336-9.471h-26.045l1.795-13.062h18.751l1.336-9.471
h-18.598l0.029-0.205C246.055,242.213,246.144,241.647,246.148,241.043z"/>
</g>
</g>
</g>
<g>
<g id="Marked_Letters_1_">
<path fill="#010101" d="M619.045,356.869h-9.961l-1.143-25.986l-14.092,25.986h-6.24l-6.709-26.807l-8.818,26.807h-10.225
l13.389-40.605h10.928l6.416,25.781l13.916-25.781h10.635L619.045,356.869z"/>
<path fill="#010101" d="M660.617,356.869h-10.811l-1.084-8.994H636.33l-4.102,8.994h-10.605l20.566-40.605h10.986L660.617,356.869
z M648.02,342.162l-1.934-15.527l-7.061,15.527H648.02z"/>
<path fill="#010101" d="M701.713,356.869h-11.338l-4.043-8.467c-1.191-2.559-2.227-4.478-3.105-5.757s-1.651-2.07-2.314-2.373
c-0.664-0.303-1.553-0.454-2.666-0.454h-1.553l-2.578,17.051h-10.283l5.889-40.605h15.85c5.761,0,9.672,1.27,11.732,3.809
s3.092,5.283,3.092,8.232c0,3.105-0.855,5.522-2.564,7.251s-4.097,2.798-7.162,3.208c1.562,0.898,2.768,1.953,3.617,3.164
s1.861,3.096,3.033,5.654L701.713,356.869z M689.525,328.012c0-1.562-0.572-2.925-1.715-4.087s-3.452-1.743-6.928-1.743h-1.611
l-1.699,11.719h4.102c2.89,0,4.922-0.61,6.094-1.831S689.525,329.496,689.525,328.012z"/>
<path fill="#010101" d="M747.705,316.264l-17.578,18.223l14.912,22.383h-11.807l-14.59-21.65l-3.281,21.65H705.02l6.064-40.605
h10.4l-2.578,17.256l16.611-17.256H747.705z"/>
<path fill="#010101" d="M780.426,316.264L779.4,323.5h-16.436l-1.26,8.818h14.268l-1.025,7.266h-14.385l-1.377,10.02h19.98
l-1.025,7.266h-30.439l5.889-40.605H780.426z"/>
<path fill="#010101" d="M823.953,336.156c0,4.512-0.86,8.301-2.578,11.367c-1.719,3.066-4.273,5.391-7.662,6.973
s-7.583,2.373-12.582,2.373H782.41l5.83-40.605h14.941c3.534,0,6.625,0.527,9.271,1.582s4.82,2.51,6.52,4.365
s2.953,3.97,3.764,6.343S823.953,333.461,823.953,336.156z M813.377,336.303c0-2.031-0.387-4.004-1.158-5.918
s-2.192-3.54-4.262-4.878c-2.07-1.338-4.941-2.007-8.613-2.007h-1.67l-3.779,26.104h5.449c2.597,0,4.828-0.391,6.693-1.172
s3.33-1.821,4.395-3.12s1.821-2.71,2.271-4.233C813.152,339.555,813.377,337.963,813.377,336.303z"/>
</g>
<g>
<path fill="#010101" d="M840.922,305.149l-3.703,26.35c-0.609,4.324-2.72,6.486-6.33,6.486c-0.773,0-1.553-0.07-2.338-0.211
l-0.756-3.023c0.879,0.188,1.67,0.281,2.373,0.281c1.219,0,2.118-0.302,2.698-0.905s0.964-1.55,1.151-2.839l3.674-26.139H840.922z
"/>
<path fill="#010101" d="M857.027,305.8l-0.475,3.357c-1.312-0.867-2.748-1.301-4.307-1.301c-1.312,0-2.361,0.34-3.146,1.02
s-1.178,1.576-1.178,2.689c0,0.551,0.188,1.131,0.562,1.74s1.471,1.644,3.287,3.103s3.015,2.739,3.595,3.841
s0.87,2.203,0.87,3.305c0,1.758-0.662,3.275-1.986,4.553s-3.088,1.916-5.291,1.916c-1.957,0-3.938-0.586-5.941-1.758l0.51-3.604
c2.238,1.559,4.16,2.338,5.766,2.338c0.867,0,1.69-0.284,2.47-0.852c0.779-0.568,1.169-1.339,1.169-2.312
c0-0.62-0.197-1.265-0.59-1.933c-0.394-0.667-1.502-1.745-3.324-3.232c-1.824-1.486-3.014-2.778-3.568-3.874
c-0.555-1.097-0.832-2.185-0.832-3.263c0-1.899,0.785-3.491,2.355-4.775c1.57-1.283,3.422-1.926,5.555-1.926
C854.016,304.833,855.516,305.155,857.027,305.8z"/>
</g>
<g>
<g>
<path d="M641,234.154c0,2.124-1.722,3.846-3.846,3.846h-5.309c-2.124,0-3.846-1.722-3.846-3.846v-0.309
c0-2.124,1.722-3.846,3.846-3.846h5.309c2.124,0,3.846,1.722,3.846,3.846V234.154z"/>
<path d="M631.859,249.5c0,1.933-1.1,3.5-2.457,3.5h-12.046c-1.356,0-2.456-1.567-2.456-3.5l0,0c0-1.933,1.1-3.5,2.456-3.5h12.046
C630.76,246,631.859,247.567,631.859,249.5L631.859,249.5z"/>
<path d="M752.044,231.021c-1.058-3.094-2.692-5.85-4.907-8.269c-2.215-2.418-5.049-4.314-8.498-5.689
c-3.45-1.375-7.479-2.063-12.086-2.063h-19.477l-7.6,52.932h24.402c6.518,0,11.984-1.032,16.402-3.094
c4.416-2.062,7.746-5.092,9.986-9.09c2.24-3.996,3.361-8.936,3.361-14.817C753.629,237.418,753.1,234.115,752.044,231.021z
M738.964,247.347c-0.586,1.986-1.573,3.826-2.96,5.519c-1.389,1.694-3.297,3.049-5.729,4.067s-5.34,1.528-8.727,1.528h-7.103
l4.927-34.027h2.176c4.787,0,8.529,0.872,11.229,2.616c2.698,1.744,4.55,3.863,5.557,6.358c1.005,2.494,1.508,5.066,1.508,7.713
C739.842,243.286,739.549,245.361,738.964,247.347z"/>
<path d="M670.148,234.043l1.373-9.61h21.424l1.337-9.434h-10.601h-24.381h-1.256c-2.124,0-3.846,1.877-3.846,4s1.722,4,3.846,4
h0.096h3.793c1.929,0,3.492,1.563,3.492,3.491c0,1.928-1.563,3.491-3.492,3.491h-4.805L657.126,230h-8.983
c-1.299,0-2.352,1.786-2.352,3.988c0,2.203,1.053,3.989,2.352,3.989h7.826L655.966,238h3.188c2.124,0,3.846,1.722,3.846,3.846
v0.309c0,2.124-1.722,3.846-3.846,3.846h-4.349h-0.96H640.2c-1.933,0-3.5,1.567-3.5,3.5s1.567,3.5,3.5,3.5h8.646h4.945h0.363
c2.124,0,3.846,1.722,3.846,3.846v0.309c0,2.124-1.722,3.846-3.846,3.846h-1.523h-3.785h-1.801c-2.124,0-3.846,1.377-3.846,3.5
s1.722,3.5,3.846,3.5h25.637c0.272,0,0.537-0.024,0.795-0.068h17.827l1.337-9.471h-26.045l1.795-13.062h18.75l1.337-9.471
h-18.599l0.029-0.205C670.055,235.213,670.144,234.647,670.148,234.043z"/>
</g>
<g>
<path fill="#010101" d="M642.236,179.15v-33.125l16.988,21.234l16.986-21.234v33.125h16.987v-57.756h-16.987l-16.986,21.234
l-16.988-21.234H625.25v57.756H642.236z"/>
<path fill="#010101" d="M756.05,150.272h-16.987v-28.878h-16.986v28.878H705.09L730.569,180L756.05,150.272z"/>
</g>
<path fill="#010101" d="M776,90H606c-11.028,0-20,8.972-20,20v170c0,11.028,8.972,20,20,20h170c11.028,0,20-8.972,20-20V110
C796,98.972,787.028,90,776,90z M606,100h170c5.514,0,10,4.486,10,10v83H596v-83C596,104.486,600.486,100,606,100z M776,290H606
c-5.514,0-10-4.486-10-10v-82h190v82C786,285.514,781.514,290,776,290z"/>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 11 KiB

View file

@ -1,32 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" id="Layer_1" x="0px" y="0px" width="300px" height="439.589px" viewBox="100 100 400 400" xml:space="preserve">
<g>
<g id="Marked_Letters">
<path fill="#010101" d="M185.044,358.869h-9.961l-1.143-25.986l-14.092,25.986h-6.24l-6.709-26.807l-8.818,26.807h-10.225 l13.389-40.605h10.928l6.416,25.781l13.916-25.781h10.635L185.044,358.869z"/>
<path fill="#010101" d="M226.617,358.869h-10.811l-1.084-8.994H202.33l-4.102,8.994h-10.605l20.566-40.605h10.986L226.617,358.869 z M214.019,344.162l-1.934-15.527l-7.061,15.527H214.019z"/>
<path fill="#010101" d="M267.712,358.869h-11.338l-4.043-8.467c-1.191-2.559-2.227-4.478-3.105-5.757s-1.651-2.07-2.314-2.373 c-0.664-0.303-1.553-0.454-2.666-0.454h-1.553l-2.578,17.051h-10.283l5.889-40.605h15.85c5.761,0,9.672,1.27,11.733,3.809 c2.061,2.539,3.091,5.283,3.091,8.232c0,3.105-0.854,5.522-2.563,7.251c-1.709,1.729-4.097,2.798-7.163,3.208 c1.562,0.898,2.769,1.953,3.618,3.164s1.86,3.096,3.032,5.654L267.712,358.869z M255.525,330.012c0-1.562-0.571-2.925-1.714-4.087 s-3.453-1.743-6.929-1.743h-1.611l-1.699,11.719h4.102c2.89,0,4.922-0.61,6.094-1.831S255.525,331.496,255.525,330.012z"/>
<path fill="#010101" d="M313.705,318.264l-17.578,18.223l14.912,22.383h-11.807l-14.59-21.65l-3.282,21.65h-10.342l6.064-40.605 h10.401l-2.578,17.256l16.611-17.256H313.705z"/>
<path fill="#010101" d="M346.426,318.264L345.4,325.5h-16.436l-1.26,8.818h14.268l-1.025,7.266h-14.385l-1.377,10.02h19.98 l-1.025,7.266h-30.439l5.889-40.605H346.426z"/>
<path fill="#010101" d="M389.953,338.156c0,4.512-0.86,8.301-2.578,11.367c-1.719,3.066-4.273,5.391-7.662,6.973 s-7.583,2.373-12.582,2.373H348.41l5.83-40.605h14.941c3.534,0,6.625,0.527,9.271,1.582s4.82,2.51,6.52,4.365 s2.953,3.97,3.764,6.343S389.953,335.461,389.953,338.156z M379.377,338.303c0-2.031-0.387-4.004-1.158-5.918 s-2.192-3.54-4.262-4.878c-2.07-1.338-4.941-2.007-8.613-2.007h-1.67l-3.779,26.104h5.449c2.597,0,4.828-0.391,6.693-1.172 s3.33-1.821,4.395-3.12s1.821-2.71,2.271-4.233C379.152,341.555,379.377,339.963,379.377,338.303z"/>
</g>
<g>
<path fill="#010101" d="M406.922,307.149l-3.703,26.35c-0.609,4.324-2.72,6.486-6.33,6.486c-0.773,0-1.553-0.07-2.338-0.211 l-0.756-3.023c0.879,0.188,1.67,0.281,2.373,0.281c1.219,0,2.118-0.302,2.698-0.905s0.964-1.55,1.151-2.839l3.674-26.139H406.922z "/>
<path fill="#010101" d="M423.027,307.8l-0.475,3.357c-1.312-0.867-2.748-1.301-4.307-1.301c-1.312,0-2.361,0.34-3.146,1.02 s-1.178,1.576-1.178,2.689c0,0.551,0.188,1.131,0.562,1.74s1.471,1.644,3.287,3.103s3.015,2.739,3.595,3.841 s0.87,2.203,0.87,3.305c0,1.758-0.662,3.275-1.986,4.553s-3.088,1.916-5.291,1.916c-1.957,0-3.938-0.586-5.941-1.758l0.51-3.604 c2.238,1.559,4.16,2.338,5.766,2.338c0.867,0,1.69-0.284,2.47-0.852c0.779-0.568,1.169-1.339,1.169-2.312 c0-0.62-0.197-1.265-0.59-1.933c-0.394-0.667-1.502-1.745-3.324-3.232c-1.824-1.486-3.014-2.778-3.568-3.874 c-0.555-1.097-0.832-2.185-0.832-3.263c0-1.899,0.785-3.491,2.355-4.775c1.57-1.283,3.422-1.926,5.555-1.926 C420.016,306.833,421.516,307.155,423.027,307.8z"/>
</g>
<g>
<g>
<path fill="#010101" d="M357,200v-92c0-8.284-6.716-15-15-15H172c-8.284,0-15,6.716-15,15v92H357z"/>
<path fill="#010101" d="M157,205v73c0,8.284,6.716,15,15,15h170c8.284,0,15-6.716,15-15v-73H157z"/>
</g>
<g>
<path fill="#FFFFFF" d="M208.737,180.15v-33.125l16.987,21.234l16.987-21.234v33.125h16.987v-57.756h-16.987l-16.987,21.234 l-16.987-21.234H191.75v57.756H208.737z"/>
<path fill="#FFFFFF" d="M322.55,151.272h-16.987v-28.878h-16.986v28.878h-16.987L297.069,181L322.55,151.272z"/>
</g>
<g>
<path fill="#FFFFFF" d="M217,241.154c0,2.124-1.722,3.846-3.845,3.846h-5.31c-2.124,0-3.845-1.722-3.845-3.846v-0.309 c0-2.124,1.722-3.846,3.845-3.846h5.31c2.124,0,3.845,1.722,3.845,3.846V241.154z"/>
<path fill="#FFFFFF" d="M207.859,256.5c0,1.933-1.1,3.5-2.457,3.5h-12.046c-1.357,0-2.457-1.567-2.457-3.5l0,0 c0-1.933,1.1-3.5,2.457-3.5h12.046C206.759,253,207.859,254.567,207.859,256.5L207.859,256.5z"/>
<path fill="#FFFFFF" d="M328.044,238.021c-1.058-3.094-2.692-5.85-4.907-8.269c-2.215-2.418-5.049-4.314-8.498-5.689 c-3.45-1.375-7.479-2.063-12.086-2.063h-19.477l-7.6,52.932h24.403c6.518,0,11.984-1.032,16.402-3.094 c4.416-2.062,7.746-5.092,9.986-9.09c2.24-3.996,3.361-8.936,3.361-14.817C329.629,244.418,329.1,241.115,328.044,238.021z M314.964,254.347c-0.586,1.986-1.573,3.826-2.96,5.519c-1.389,1.694-3.297,3.049-5.729,4.067s-5.34,1.528-8.727,1.528h-7.103 l4.927-34.027h2.176c4.787,0,8.529,0.872,11.229,2.616c2.698,1.744,4.55,3.863,5.557,6.358c1.005,2.494,1.508,5.066,1.508,7.713 C315.842,250.286,315.549,252.361,314.964,254.347z"/>
<path fill="#FFFFFF" d="M246.148,241.043l1.373-9.61h21.424l1.336-9.434h-10.601h-24.38h-1.256c-2.125,0-3.846,1.877-3.846,4 s1.721,4,3.846,4h0.096h3.793c1.928,0,3.491,1.563,3.491,3.491c0,1.928-1.563,3.491-3.491,3.491h-4.806L233.126,237h-8.983 c-1.299,0-2.353,1.786-2.353,3.988c0,2.203,1.053,3.989,2.353,3.989h7.826L231.966,245h3.189c2.124,0,3.845,1.722,3.845,3.846 v0.309c0,2.124-1.722,3.846-3.845,3.846h-4.349h-0.96H216.2c-1.933,0-3.5,1.567-3.5,3.5s1.567,3.5,3.5,3.5h8.645h4.945h0.364 c2.124,0,3.845,1.722,3.845,3.846v0.309c0,2.124-1.722,3.846-3.845,3.846h-1.524h-3.785h-1.8c-2.125,0-3.846,1.377-3.846,3.5 s1.721,3.5,3.846,3.5h25.636c0.273,0,0.538-0.024,0.795-0.068h17.828l1.336-9.471h-26.045l1.795-13.062h18.751l1.336-9.471 h-18.598l0.029-0.205C246.055,242.213,246.144,241.647,246.148,241.043z"/>
</g>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 5.5 KiB

View file

@ -1,270 +0,0 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8"/>
<title>Marked.js Documentation</title>
<style>
body {
font-family: -apple-system,BlinkMacSystemFont,"Segoe UI",Helvetica,Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol";
font-size: 16px;
line-height: 1.5;
word-wrap: break-word;
background: #F9F9F9;
}
#container {
max-width: 900px;
margin: auto;
}
#content {
padding: 5px 10px;
border: 1px solid #ddd;
border-radius: 3px;
background: white;
}
#content h1:first-child {
margin-top: 0px;
}
nav {
border: 1px solid #ddd;
border-radius: 3px;
background: white;
margin-right: 10px;
}
nav > ul {
position: sticky;
top: 5px;
margin: 10px 0px;
padding: 0;
list-style-type: none;
font-size: 14px;
}
nav > ul > li {
min-width: 125px;
padding: 0px 15px;
}
nav > ul > li > ul {
padding-left: 25px;
}
nav > ul > li > ul > li {
font-size: 0.8em;
}
nav .selected {
color: #111;
font-weight: bold;
}
nav .selected:hover {
text-decoration: none;
}
header {
display: flex;
height: 50px;
}
header h1 { margin: 0; }
table {
border-spacing: 0;
border-collapse: collapse;
border: 1px solid #ddd;
}
td, th {
border: 1px solid #ddd;
padding: 5px;
}
a {
color: #0366d6;
text-decoration: none;
}
a:hover {
text-decoration: underline;
}
pre {
font-family: "SFMono-Regular",Consolas,"Liberation Mono",Menlo,Courier,monospace;
padding: 16px;
overflow: auto;
font-size: 85%;
line-height: 1.45;
background-color: #f6f8fa;
border-radius: 3px;
}
code:not([class]) {
padding: 0.2em 0.4em;
margin: 0;
font-size: 85%;
background-color: rgba(27,31,35,0.05);
border-radius: 3px;
}
.github-corner:hover .octo-arm{animation:octocat-wave 560ms ease-in-out}@keyframes octocat-wave{0%,100%{transform:rotate(0)}20%,60%{transform:rotate(-25deg)}40%,80%{transform:rotate(10deg)}}@media (max-width:500px){.github-corner:hover .octo-arm{animation:none}.github-corner .octo-arm{animation:octocat-wave 560ms ease-in-out}}
</style>
</head>
<body>
<a href="https://github.com/markedjs/marked" class="github-corner" aria-label="View source on Github">
<svg width="80" height="80" viewBox="0 0 250 250" style="fill:#202020; color:#fff; position: absolute; top: 0; border: 0; right: 0;" aria-hidden="true">
<path d="M0,0 L115,115 L130,115 L142,142 L250,250 L250,0 Z"></path>
<path d="M128.3,109.0 C113.8,99.7 119.0,89.6 119.0,89.6 C122.0,82.7 120.5,78.6 120.5,78.6 C119.2,72.0 123.4,76.3 123.4,76.3 C127.3,80.9 125.5,87.3 125.5,87.3 C122.9,97.6 130.6,101.9 134.4,103.2" fill="currentColor" style="transform-origin: 130px 106px;" class="octo-arm"></path>
<path d="M115.0,115.0 C114.9,115.1 118.7,116.5 119.8,115.4 L133.7,101.6 C136.9,99.2 139.9,98.4 142.2,98.6 C133.8,88.0 127.5,74.4 143.8,58.0 C148.5,53.4 154.0,51.2 159.7,51.0 C160.3,49.4 163.2,43.6 171.4,40.1 C171.4,40.1 176.1,42.5 178.8,56.2 C183.1,58.6 187.2,61.8 190.9,65.4 C194.5,69.0 197.7,73.2 200.1,77.6 C213.8,80.2 216.3,84.9 216.3,84.9 C212.7,93.1 206.9,96.0 205.4,96.6 C205.1,102.4 203.0,107.8 198.3,112.5 C181.9,128.9 168.3,122.5 157.7,114.1 C157.9,116.9 156.7,120.9 152.7,124.9 L141.0,136.5 C139.8,137.7 141.6,141.9 141.8,141.8 Z" fill="currentColor" class="octo-body"></path>
</svg>
</a>
<div id="container">
<header>
<a href="#/README.md">
<img src="img/logo-black.svg" height="64px" width="64px" />
</a>
<h1>Marked.js Documentation</h1>
</header>
<div style="display: flex">
<nav>
<ul>
<li>
<a href="#/README.md">Getting Started</a>
<ul>
<li><a href="#/README.md#demo">Demo</a></li>
<li><a href="#/README.md#installation">Installation</a></li>
<li><a href="#/README.md#usage">Usage</a></li>
<li><a href="#/README.md#specifications">Specs</a></li>
<li><a href="#/README.md#security">Security</a></li>
</ul>
</li>
<li>
<a href="#/USING_ADVANCED.md">Advanced Usage</a>
<ul>
<li><a href="#/USING_ADVANCED.md#options">Options</a></li>
<li><a href="#/USING_ADVANCED.md#highlight">Highlighting</a></li>
<li><a href="#/USING_ADVANCED.md#workers">Workers</a></li>
</ul>
</li>
<li>
<a href="#/USING_PRO.md">Extensibility</a>
<ul>
<li><a href="#/USING_PRO.md#renderer">Renderer</a></li>
<li><a href="#/USING_PRO.md#lexer">Lexer</a></li>
<li><a href="#/USING_PRO.md#parser">Parser</a></li>
</ul>
</li>
<li>
<a href="#/CONTRIBUTING.md">Contributing</a>
<ul>
<li><a href="#/CONTRIBUTING.md#design-principles">Design Principles</a></li>
<li><a href="#/CONTRIBUTING.md#priorities">Priorities</a></li>
<li><a href="#/CONTRIBUTING.md#test-early-often-and-everything">Testing</a></li>
</ul>
</li>
<li><a href="#/CODE_OF_CONDUCT.md">Code of Conduct</a></li>
<li><a href="#/AUTHORS.md">Authors</a></li>
<li><a href="#/PUBLISHING.md">Publishing</a></li>
<li><a href="https://github.com/markedjs/marked/blob/master/LICENSE.md">License</a></li>
</ul>
</nav>
<div id="content"></div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/es6-promise/dist/es6-promise.js"></script>
<script src="https://cdn.jsdelivr.net/npm/unfetch/dist/unfetch.umd.js"></script>
<script>
if (!window.Promise) {
window.Promise = ES6Promise;
}
if (!window.fetch) {
window.fetch = unfetch;
}
var content = document.querySelector('#content');
var body = document.querySelector('html');
var navLinks = document.querySelectorAll('nav a');
var currentPage = 'README.md';
var currentHash = '';
var renderedPage = '';
function hashChange() {
var hash = location.hash.slice(1);
if (!hash) {
hash = 'README.md';
}
var uri = hash.split('#');
if (uri[0].match(/^\//)) {
currentPage = uri[0].slice(1);
if (uri.length > 1) {
currentHash = uri[1];
} else {
currentHash = '';
}
} else {
currentHash = uri[0];
}
fetchPage(currentPage).then(function () {
fetchAnchor(currentHash)
});
var url = '#/' + currentPage + (currentHash ? '#' + currentHash : '');
var fullUrl = window.location.origin + '/' + url;
navLinks.forEach(function(link) {
link.className = link.href === fullUrl ? 'selected' : '';
});
history.replaceState('', document.title, url);
}
function fetchAnchor(anchor) {
if (!anchor) {
return;
}
var hashElement = document.getElementById(anchor);
if (hashElement) {
hashElement.scrollIntoView();
}
}
function fetchPage(page) {
if (page === renderedPage) {
return Promise.resolve();
}
return fetch(page)
.then(function (res) {
if (!res.ok) {
throw new Error('Error ' + res.status + ': ' + res.statusText);
}
return res.text();
})
.then(function (text) {
renderedPage = page;
content.innerHTML = marked(text);
body.scrollTop = 0;
}).catch(function (e) {
content.innerHTML = '<p>Oops! There was a problem rendering the page.</p>'
+ '<p>' + e.message + '</p>';
});
}
window.addEventListener('hashchange', function (e) {
e.preventDefault();
hashChange();
});
hashChange();
</script>
</body>
</html>

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -1,111 +0,0 @@
.ds q \N'34'
.TH marked 1
.SH NAME
marked \- a javascript markdown parser
.SH SYNOPSIS
.B marked
[\-o \fI<output>\fP] [\-i \fI<input>\fP] [\-\-help]
[\-\-tokens] [\-\-pedantic] [\-\-gfm]
[\-\-breaks] [\-\-sanitize]
[\-\-smart\-lists] [\-\-lang\-prefix \fI<prefix>\fP]
[\-\-no\-etc...] [\-\-silent] [\fIfilename\fP]
.SH DESCRIPTION
.B marked
is a full-featured javascript markdown parser, built for speed.
It also includes multiple GFM features.
.SH EXAMPLES
.TP
cat in.md | marked > out.html
.TP
echo "hello *world*" | marked
.TP
marked \-o out.html \-i in.md \-\-gfm
.TP
marked \-\-output="hello world.html" \-i in.md \-\-no-breaks
.SH OPTIONS
.TP
.BI \-o,\ \-\-output\ [\fIoutput\fP]
Specify file output. If none is specified, write to stdout.
.TP
.BI \-i,\ \-\-input\ [\fIinput\fP]
Specify file input, otherwise use last argument as input file.
If no input file is specified, read from stdin.
.TP
.BI \-\-test
Makes sure the test(s) pass.
.RS
.PP
.B \-\-glob [\fIfile\fP]
Specify which test to use.
.PP
.B \-\-fix
Fixes tests.
.PP
.B \-\-bench
Benchmarks the test(s).
.PP
.B \-\-time
Times The test(s).
.PP
.B \-\-minified
Runs test file(s) as minified.
.PP
.B \-\-stop
Stop process if a test fails.
.RE
.TP
.BI \-t,\ \-\-tokens
Output a token stream instead of html.
.TP
.BI \-\-pedantic
Conform to obscure parts of markdown.pl as much as possible.
Don't fix original markdown bugs.
.TP
.BI \-\-gfm
Enable github flavored markdown.
.TP
.BI \-\-breaks
Enable GFM line breaks. Only works with the gfm option.
.TP
.BI \-\-sanitize
Sanitize output. Ignore any HTML input.
.TP
.BI \-\-smart\-lists
Use smarter list behavior than the original markdown.
.TP
.BI \-\-lang\-prefix\ [\fIprefix\fP]
Set the prefix for code block classes.
.TP
.BI \-\-mangle
Mangle email addresses.
.TP
.BI \-\-no\-sanitize,\ \-no-etc...
The inverse of any of the marked options above.
.TP
.BI \-\-silent
Silence error output.
.TP
.BI \-h,\ \-\-help
Display help information.
.SH CONFIGURATION
For configuring and running programmatically.
.B Example
require('marked')('*foo*', { gfm: true });
.SH BUGS
Please report any bugs to https://github.com/markedjs/marked.
.SH LICENSE
Copyright (c) 2011-2014, Christopher Jeffrey (MIT License).
.SH "SEE ALSO"
.BR markdown(1),
.BR node.js(1)

View file

@ -1,96 +0,0 @@
marked(1) General Commands Manual marked(1)
NAME
marked - a javascript markdown parser
SYNOPSIS
marked [-o <output>] [-i <input>] [--help] [--tokens] [--pedantic]
[--gfm] [--breaks] [--sanitize] [--smart-lists] [--lang-prefix <pre-
fix>] [--no-etc...] [--silent] [filename]
DESCRIPTION
marked is a full-featured javascript markdown parser, built for speed.
It also includes multiple GFM features.
EXAMPLES
cat in.md | marked > out.html
echo "hello *world*" | marked
marked -o out.html -i in.md --gfm
marked --output="hello world.html" -i in.md --no-breaks
OPTIONS
-o, --output [output]
Specify file output. If none is specified, write to stdout.
-i, --input [input]
Specify file input, otherwise use last argument as input file.
If no input file is specified, read from stdin.
--test Makes sure the test(s) pass.
--glob [file] Specify which test to use.
--fix Fixes tests.
--bench Benchmarks the test(s).
--time Times The test(s).
--minified Runs test file(s) as minified.
--stop Stop process if a test fails.
-t, --tokens
Output a token stream instead of html.
--pedantic
Conform to obscure parts of markdown.pl as much as possible.
Don't fix original markdown bugs.
--gfm Enable github flavored markdown.
--breaks
Enable GFM line breaks. Only works with the gfm option.
--sanitize
Sanitize output. Ignore any HTML input.
--smart-lists
Use smarter list behavior than the original markdown.
--lang-prefix [prefix]
Set the prefix for code block classes.
--mangle
Mangle email addresses.
--no-sanitize, -no-etc...
The inverse of any of the marked options above.
--silent
Silence error output.
-h, --help
Display help information.
CONFIGURATION
For configuring and running programmatically.
Example
require('marked')('*foo*', { gfm: true });
BUGS
Please report any bugs to https://github.com/markedjs/marked.
LICENSE
Copyright (c) 2011-2014, Christopher Jeffrey (MIT License).
SEE ALSO
markdown(1), node.js(1)
marked(1)

File diff suppressed because it is too large Load diff

View file

@ -1,77 +0,0 @@
{
"name": "marked",
"description": "A markdown parser built for speed",
"author": "Christopher Jeffrey",
"version": "0.8.0",
"main": "./src/marked.js",
"bin": "./bin/marked",
"man": "./man/marked.1",
"files": [
"bin/",
"lib/",
"src/",
"man/",
"marked.min.js"
],
"repository": "git://github.com/markedjs/marked.git",
"homepage": "https://marked.js.org",
"bugs": {
"url": "http://github.com/markedjs/marked/issues"
},
"license": "MIT",
"keywords": [
"markdown",
"markup",
"html"
],
"tags": [
"markdown",
"markup",
"html"
],
"devDependencies": {
"@babel/core": "^7.8.7",
"@babel/preset-env": "^7.8.7",
"@markedjs/html-differ": "^3.0.0",
"cheerio": "^0.22.0",
"commonmark": "^0.29.1",
"eslint": "^6.8.0",
"eslint-config-standard": "^14.1.0",
"eslint-plugin-import": "^2.20.1",
"eslint-plugin-node": "^11.0.0",
"eslint-plugin-promise": "^4.2.1",
"eslint-plugin-standard": "^4.0.1",
"front-matter": "^3.1.0",
"jasmine": "^3.5.0",
"markdown": "^0.5.0",
"markdown-it": "^10.0.0",
"node-fetch": "^2.6.0",
"rollup": "^2.0.6",
"rollup-plugin-babel": "^4.4.0",
"rollup-plugin-commonjs": "^10.1.0",
"rollup-plugin-license": "^0.13.0",
"uglify-js": "^3.8.0",
"vuln-regex-detector": "^1.3.0"
},
"scripts": {
"test": "jasmine --config=jasmine.json",
"test:all": "npm test && npm run test:lint",
"test:unit": "npm test -- test/unit/**/*-spec.js",
"test:specs": "npm test -- test/specs/**/*-spec.js",
"test:lint": "eslint bin/marked .",
"test:redos": "node test/vuln-regex.js",
"test:update": "node test/update-specs.js",
"bench": "npm run rollup && node test/bench.js",
"lint": "eslint --fix bin/marked .",
"build:reset": "git checkout upstream/master lib/marked.js lib/marked.esm.js marked.min.js",
"build": "npm run rollup && npm run minify",
"rollup": "npm run rollup:umd && npm run rollup:esm",
"rollup:umd": "rollup -c rollup.config.js",
"rollup:esm": "rollup -c rollup.config.esm.js",
"minify": "uglifyjs lib/marked.js -cm --comments /Copyright/ -o marked.min.js",
"preversion": "npm run build && (git diff --quiet || git commit -am 'build')"
},
"engines": {
"node": ">= 8.16.2"
}
}

18
packages/markdown/package.js Executable file → Normal file
View file

@ -1,24 +1,22 @@
// Source: https://github.com/chjj/marked
Package.describe({
name: 'wekan-markdown',
summary: "GitHub flavored markdown parser for Meteor based on marked.js",
version: "1.0.7",
git: "https://github.com/wekan/markdown.git"
summary: 'GitHub flavored markdown parser for Meteor based on markdown-it',
version: '1.0.9',
git: 'https://github.com/wekan/markdown.git',
});
// Before Meteor 0.9?
if(!Package.onUse) Package.onUse = Package.on_use;
Package.onUse(function (api) {
if(api.versionsFrom) api.versionsFrom('METEOR@0.9.0');
if(api.versionsFrom) api.versionsFrom('1.8.2');
api.use('templating');
api.use("ecmascript", ['server', 'client']);
api.add_files('marked/lib/marked.js', ['server', 'client']);
api.add_files('markdown.js', ['server', 'client']);
api.export('Markdown', ['server', 'client']);
api.use("ui", "client", {weak: true});
api.add_files("template-integration.js", "client");
api.use('ui', 'client', {weak: true});
api.add_files('src/template-integration.js', 'client');
});

0
packages/markdown/smart.json Executable file → Normal file
View file

View file

@ -0,0 +1,78 @@
import DOMPurify from 'dompurify';
var Markdown = require('markdown-it')({
html: true,
linkify: true,
typographer: true,
breaks: true,
});
import markdownItMermaid from "@liradb2000/markdown-it-mermaid";
/*
// Static URL Scheme Listing
var urlschemes = [
"aodroplink",
"thunderlink",
"cbthunderlink",
"onenote",
"file",
"abasurl",
"conisio",
"mailspring"
];
// Better would be a field in the admin backend to set this dynamically
// instead of putting all known or wanted url schemes here hard into code
// but i was not able to access those settings
// var urlschemes = currentSetting.automaticLinkedUrlSchemes.split('\n');
// put all url schemes into the linkify configuration to automatically make it clickable
for(var i=0; i<urlschemes.length;i++){
//console.log("adding autolink for "+urlschemes[i]);
Markdown.linkify.add(urlschemes[i]+":",'http:');
}
// Additional safeAttrValue function to allow for other specific protocols
// See https://github.com/leizongmin/js-xss/issues/52#issuecomment-241354114
function mySafeAttrValue(tag, name, value, cssFilter) {
// only when the tag is 'a' and attribute is 'href'
// then use your custom function
if (tag === 'a' && name === 'href') {
// only filter the value if starts with an registered url scheme
urlscheme = value.split(/:/);
//console.log("validating "+urlscheme[0]);
if(urlschemes.includes(urlscheme[0])) return value;
else {
// use the default safeAttrValue function to process all non cbthunderlinks
return sanitizeXss.safeAttrValue(tag, name, value, cssFilter);
}
// } else if (tag === 'svg') {
// return `<img src="data:image/svg+xml;base64,` + atob(value) + `"></img>`;
} else {
// use the default safeAttrValue function to process it
return sanitizeXss.safeAttrValue(tag, name, value, cssFilter);
}
};
*/
var emoji = require('markdown-it-emoji');
Markdown.use(emoji);
Markdown.use(markdownItMermaid);
if (Package.ui) {
const Template = Package.templating.Template;
const UI = Package.ui.UI;
const HTML = Package.htmljs.HTML;
const Blaze = Package.blaze.Blaze; // implied by `ui`
UI.registerHelper('markdown', new Template('markdown', function () {
const self = this;
let text = '';
if (self.templateContentBlock) {
text = Blaze._toText(self.templateContentBlock, HTML.TEXTMODE.STRING);
}
return HTML.Raw(DOMPurify.sanitize(Markdown.render(text), {ALLOW_UNKNOWN_PROTOCOLS: true}));
}));
}

View file

@ -1,16 +0,0 @@
if (Package.ui) {
var Template = Package.templating.Template;
var UI = Package.ui.UI;
var HTML = Package.htmljs.HTML;
var Blaze = Package.blaze.Blaze; // implied by `ui`
UI.registerHelper('markdown', new Template('markdown', function () {
var self = this;
var text = "";
if(self.templateContentBlock) {
text = Blaze._toText(self.templateContentBlock, HTML.TEXTMODE.STRING);
}
return HTML.Raw(Markdown(text));
}));
}

View file

@ -229,13 +229,13 @@ const casValidate = (req, ticket, token, service, callback) => {
if (attrs.debug) {
console.log(`CAS response : ${JSON.stringify(result)}`);
}
let user = Meteor.users.findOne({ 'username': options.username });
let user = Users.findOne({ 'username': options.username });
if (! user) {
if (attrs.debug) {
console.log(`Creating user account ${JSON.stringify(options)}`);
}
const userId = Accounts.insertUserDoc({}, options);
user = Meteor.users.findOne(userId);
user = Users.findOne(userId);
}
if (attrs.debug) {
console.log(`Using user account ${JSON.stringify(user)}`);

View file

@ -0,0 +1,25 @@
# Contributing guide
Want to contribute to Accounts-Lockout? Awesome!
There are many ways you can contribute, see below.
## Opening issues
Open an issue to report bugs or to propose new features.
- Reporting bugs: describe the bug as clearly as you can, including steps to reproduce, what happened and what you were expecting to happen. Also include browser version, OS and other related software's (npm, Node.js, etc) versions when applicable.
- Proposing features: explain the proposed feature, what it should do, why it is useful, how users should use it. Give us as much info as possible so it will be easier to discuss, access and implement the proposed feature. When you're unsure about a certain aspect of the feature, feel free to leave it open for others to discuss and find an appropriate solution.
## Proposing pull requests
Pull requests are very welcome. Note that if you are going to propose drastic changes, be sure to open an issue for discussion first, to make sure that your PR will be accepted before you spend effort coding it.
Fork the Accounts-Lockout repository, clone it locally and create a branch for your proposed bug fix or new feature. Avoid working directly on the master branch.
Implement your bug fix or feature, write tests to cover it and make sure all tests are passing (run a final `npm test` to make sure everything is correct). Then commit your changes, push your bug fix/feature branch to the origin (your forked repo) and open a pull request to the upstream (the repository you originally forked)'s master branch.
## Documentation
Documentation is extremely important and takes a fair deal of time and effort to write and keep updated.
Please submit any and all improvements you can make to the repository's docs.

View file

@ -1,6 +1,6 @@
The MIT License (MIT)
Copyright (c) 2014-2019 The Wekan Team
Copyright (c) 2017 Lucas Antoniassi de Paiva
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

View file

@ -0,0 +1,126 @@
# Meteor - Accounts - Lockout
[![Build Status](https://travis-ci.org/LucasAntoniassi/meteor-accounts-lockout.svg?branch=master)](https://travis-ci.org/LucasAntoniassi/meteor-accounts-lockout)
[![Codacy Badge](https://api.codacy.com/project/badge/Grade/8ce60fa7e2c24891b9bdfc3b65433d23)](https://www.codacy.com/app/lucasantoniassi/meteor-accounts-lockout?utm_source=github.com&amp;utm_medium=referral&amp;utm_content=LucasAntoniassi/meteor-accounts-lockout&amp;utm_campaign=Badge_Grade)
[![Code Climate](https://codeclimate.com/github/LucasAntoniassi/meteor-accounts-lockout/badges/gpa.svg)](https://codeclimate.com/github/LucasAntoniassi/meteor-accounts-lockout)
## What it is
Seamless Meteor apps accounts protection from password brute-force attacks.
Users won't notice it. Hackers shall not pass.
![you-shall-not-pass](https://cloud.githubusercontent.com/assets/3399956/9023729/007dd2a2-38b1-11e5-807a-b81c6ce00c80.jpg)
## Installation
```
meteor add lucasantoniassi:accounts-lockout
```
## Usage via ES6 import
```javascript
// server
import { AccountsLockout } from 'meteor/lucasantoniassi:accounts-lockout';
```
## How to use
Default settings:
```javascript
"knownUsers": {
"failuresBeforeLockout": 3, // positive integer greater than 0
"lockoutPeriod": 60, // in seconds
"failureWindow": 10 // in seconds
},
"unknownUsers": {
"failuresBeforeLockout": 3, // positive integer greater than 0
"lockoutPeriod": 60, // in seconds
"failureWindow": 10 // in seconds
}
```
`knownUsers` are users where already belongs to your `Meteor.users` collections,
these rules are applied if they attempt to login with an incorrect password but a know email.
`unknownUsers` are users where **not** belongs to your `Meteor.users` collections,
these rules are applied if they attempt to login with a unknown email.
`failuresBeforeLockout` should be a positive integer greater than 0.
`lockoutPeriod` should be in seconds.
`failureWindow` should be in seconds.
If the `default` is nice to you, you can do that.
```javascript
(new AccountsLockout()).startup();
```
You can overwrite passing an `object` as argument.
```javascript
(new AccountsLockout({
knownUsers: {
failuresBeforeLockout: 3,
lockoutPeriod: 60,
failureWindow: 15,
},
unknownUsers: {
failuresBeforeLockout: 3,
lockoutPeriod: 60,
failureWindow: 15,
},
})).startup();
```
If you prefer, you can pass a `function` as argument.
```javascript
const knownUsersRules = (user) => {
// apply some logic with this user
return {
failuresBeforeLockout,
lockoutPeriod,
failureWindow,
};
};
const unknownUsersRules = (connection) => {
// apply some logic with this connection
return {
failuresBeforeLockout,
lockoutPeriod,
failureWindow,
};
};
(new AccountsLockout({
knownUsers: knownUsersRules,
unknownUsers: unknownUsersRules,
})).startup();
```
If you prefer, you can use `Meteor.settings`. It will overwrite any previous case.
```javascript
"accounts-lockout": {
"knownUsers": {
"failuresBeforeLockout": 3,
"lockoutPeriod": 60,
"failureWindow": 10
},
"unknownUsers": {
"failuresBeforeLockout": 3,
"lockoutPeriod": 60,
"failureWindow": 10
}
}
```
## License
This package is open-sourced software licensed under the [MIT license](http://opensource.org/licenses/MIT).

View file

@ -0,0 +1,5 @@
import AccountsLockout from './src/accountsLockout';
const Name = 'wekan-accounts-lockout';
export { Name, AccountsLockout };

View file

@ -0,0 +1,18 @@
/* global Package */
Package.describe({
name: 'wekan-accounts-lockout',
version: '1.0.0',
summary: 'Meteor package for locking user accounts and stopping brute force attacks',
git: 'https://github.com/lucasantoniassi/meteor-accounts-lockout.git',
documentation: 'README.md',
});
Package.onUse((api) => {
api.versionsFrom('1.4.2.3');
api.use([
'ecmascript',
'accounts-password',
]);
api.mainModule('accounts-lockout.js');
});

View file

@ -0,0 +1,4 @@
{
"name": "wekan-accounts-lockout",
"private": true
}

View file

@ -0,0 +1,29 @@
import KnownUser from './knownUser';
import UnknownUser from './unknownUser';
class AccountsLockout {
constructor({
knownUsers = {
failuresBeforeLockout: 3,
lockoutPeriod: 60,
failureWindow: 15,
},
unknownUsers = {
failuresBeforeLockout: 3,
lockoutPeriod: 60,
failureWindow: 15,
},
}) {
this.settings = {
knownUsers,
unknownUsers,
};
}
startup() {
(new KnownUser(this.settings.knownUsers)).startup();
(new UnknownUser(this.settings.unknownUsers)).startup();
}
}
export default AccountsLockout;

View file

@ -0,0 +1,3 @@
import { Meteor } from 'meteor/meteor';
export default new Meteor.Collection('AccountsLockout.Connections');

View file

@ -0,0 +1,321 @@
/* eslint-disable no-underscore-dangle */
import { Meteor } from 'meteor/meteor';
import { Accounts } from 'meteor/accounts-base';
class KnownUser {
constructor(settings) {
this.unchangedSettings = settings;
this.settings = settings;
}
startup() {
if (!(this.unchangedSettings instanceof Function)) {
this.updateSettings();
}
this.scheduleUnlocksForLockedAccounts();
KnownUser.unlockAccountsIfLockoutAlreadyExpired();
this.hookIntoAccounts();
}
updateSettings() {
const settings = KnownUser.knownUsers();
if (settings) {
settings.forEach(function updateSetting({ key, value }) {
this.settings[key] = value;
});
}
this.validateSettings();
}
validateSettings() {
if (
!this.settings.failuresBeforeLockout ||
this.settings.failuresBeforeLockout < 0
) {
throw new Error('"failuresBeforeLockout" is not positive integer');
}
if (
!this.settings.lockoutPeriod ||
this.settings.lockoutPeriod < 0
) {
throw new Error('"lockoutPeriod" is not positive integer');
}
if (
!this.settings.failureWindow ||
this.settings.failureWindow < 0
) {
throw new Error('"failureWindow" is not positive integer');
}
}
scheduleUnlocksForLockedAccounts() {
const lockedAccountsCursor = Meteor.users.find(
{
'services.accounts-lockout.unlockTime': {
$gt: Number(new Date()),
},
},
{
fields: {
'services.accounts-lockout.unlockTime': 1,
},
},
);
const currentTime = Number(new Date());
lockedAccountsCursor.forEach((user) => {
let lockDuration = KnownUser.unlockTime(user) - currentTime;
if (lockDuration >= this.settings.lockoutPeriod) {
lockDuration = this.settings.lockoutPeriod * 1000;
}
if (lockDuration <= 1) {
lockDuration = 1;
}
Meteor.setTimeout(
KnownUser.unlockAccount.bind(null, user._id),
lockDuration,
);
});
}
static unlockAccountsIfLockoutAlreadyExpired() {
const currentTime = Number(new Date());
const query = {
'services.accounts-lockout.unlockTime': {
$lt: currentTime,
},
};
const data = {
$unset: {
'services.accounts-lockout.unlockTime': 0,
'services.accounts-lockout.failedAttempts': 0,
},
};
Meteor.users.update(query, data);
}
hookIntoAccounts() {
Accounts.validateLoginAttempt(this.validateLoginAttempt.bind(this));
Accounts.onLogin(KnownUser.onLogin);
}
validateLoginAttempt(loginInfo) {
if (
// don't interrupt non-password logins
loginInfo.type !== 'password' ||
loginInfo.user === undefined ||
// Don't handle errors unless they are due to incorrect password
(loginInfo.error !== undefined && loginInfo.error.reason !== 'Incorrect password')
) {
return loginInfo.allowed;
}
// If there was no login error and the account is NOT locked, don't interrupt
const unlockTime = KnownUser.unlockTime(loginInfo.user);
if (loginInfo.error === undefined && unlockTime === 0) {
return loginInfo.allowed;
}
if (this.unchangedSettings instanceof Function) {
this.settings = this.unchangedSettings(loginInfo.user);
this.validateSettings();
}
const userId = loginInfo.user._id;
let failedAttempts = 1 + KnownUser.failedAttempts(loginInfo.user);
const firstFailedAttempt = KnownUser.firstFailedAttempt(loginInfo.user);
const currentTime = Number(new Date());
const canReset = (currentTime - firstFailedAttempt) > (1000 * this.settings.failureWindow);
if (canReset) {
failedAttempts = 1;
KnownUser.resetAttempts(failedAttempts, userId);
}
const canIncrement = failedAttempts < this.settings.failuresBeforeLockout;
if (canIncrement) {
KnownUser.incrementAttempts(failedAttempts, userId);
}
const maxAttemptsAllowed = this.settings.failuresBeforeLockout;
const attemptsRemaining = maxAttemptsAllowed - failedAttempts;
if (unlockTime > currentTime) {
let duration = unlockTime - currentTime;
duration = Math.ceil(duration / 1000);
duration = duration > 1 ? duration : 1;
KnownUser.tooManyAttempts(duration);
}
if (failedAttempts === maxAttemptsAllowed) {
this.setNewUnlockTime(failedAttempts, userId);
let duration = this.settings.lockoutPeriod;
duration = Math.ceil(duration);
duration = duration > 1 ? duration : 1;
return KnownUser.tooManyAttempts(duration);
}
return KnownUser.incorrectPassword(
failedAttempts,
maxAttemptsAllowed,
attemptsRemaining,
);
}
static resetAttempts(
failedAttempts,
userId,
) {
const currentTime = Number(new Date());
const query = { _id: userId };
const data = {
$set: {
'services.accounts-lockout.failedAttempts': failedAttempts,
'services.accounts-lockout.lastFailedAttempt': currentTime,
'services.accounts-lockout.firstFailedAttempt': currentTime,
},
};
Meteor.users.update(query, data);
}
static incrementAttempts(
failedAttempts,
userId,
) {
const currentTime = Number(new Date());
const query = { _id: userId };
const data = {
$set: {
'services.accounts-lockout.failedAttempts': failedAttempts,
'services.accounts-lockout.lastFailedAttempt': currentTime,
},
};
Meteor.users.update(query, data);
}
setNewUnlockTime(
failedAttempts,
userId,
) {
const currentTime = Number(new Date());
const newUnlockTime = (1000 * this.settings.lockoutPeriod) + currentTime;
const query = { _id: userId };
const data = {
$set: {
'services.accounts-lockout.failedAttempts': failedAttempts,
'services.accounts-lockout.lastFailedAttempt': currentTime,
'services.accounts-lockout.unlockTime': newUnlockTime,
},
};
Meteor.users.update(query, data);
Meteor.setTimeout(
KnownUser.unlockAccount.bind(null, userId),
this.settings.lockoutPeriod * 1000,
);
}
static onLogin(loginInfo) {
if (loginInfo.type !== 'password') {
return;
}
const userId = loginInfo.user._id;
const query = { _id: userId };
const data = {
$unset: {
'services.accounts-lockout.unlockTime': 0,
'services.accounts-lockout.failedAttempts': 0,
},
};
Meteor.users.update(query, data);
}
static incorrectPassword(
failedAttempts,
maxAttemptsAllowed,
attemptsRemaining,
) {
throw new Meteor.Error(
403,
'Incorrect password',
JSON.stringify({
message: 'Incorrect password',
failedAttempts,
maxAttemptsAllowed,
attemptsRemaining,
}),
);
}
static tooManyAttempts(duration) {
throw new Meteor.Error(
403,
'Too many attempts',
JSON.stringify({
message: 'Wrong passwords were submitted too many times. Account is locked for a while.',
duration,
}),
);
}
static knownUsers() {
let knownUsers;
try {
knownUsers = Meteor.settings['accounts-lockout'].knownUsers;
} catch (e) {
knownUsers = false;
}
return knownUsers || false;
}
static unlockTime(user) {
let unlockTime;
try {
unlockTime = user.services['accounts-lockout'].unlockTime;
} catch (e) {
unlockTime = 0;
}
return unlockTime || 0;
}
static failedAttempts(user) {
let failedAttempts;
try {
failedAttempts = user.services['accounts-lockout'].failedAttempts;
} catch (e) {
failedAttempts = 0;
}
return failedAttempts || 0;
}
static lastFailedAttempt(user) {
let lastFailedAttempt;
try {
lastFailedAttempt = user.services['accounts-lockout'].lastFailedAttempt;
} catch (e) {
lastFailedAttempt = 0;
}
return lastFailedAttempt || 0;
}
static firstFailedAttempt(user) {
let firstFailedAttempt;
try {
firstFailedAttempt = user.services['accounts-lockout'].firstFailedAttempt;
} catch (e) {
firstFailedAttempt = 0;
}
return firstFailedAttempt || 0;
}
static unlockAccount(userId) {
const query = { _id: userId };
const data = {
$unset: {
'services.accounts-lockout.unlockTime': 0,
'services.accounts-lockout.failedAttempts': 0,
},
};
Meteor.users.update(query, data);
}
}
export default KnownUser;

View file

@ -0,0 +1,329 @@
import { Meteor } from 'meteor/meteor';
import { Accounts } from 'meteor/accounts-base';
import _AccountsLockoutCollection from './accountsLockoutCollection';
class UnknownUser {
constructor(
settings,
{
AccountsLockoutCollection = _AccountsLockoutCollection,
} = {},
) {
this.AccountsLockoutCollection = AccountsLockoutCollection;
this.settings = settings;
}
startup() {
if (!(this.settings instanceof Function)) {
this.updateSettings();
}
this.scheduleUnlocksForLockedAccounts();
this.unlockAccountsIfLockoutAlreadyExpired();
this.hookIntoAccounts();
}
updateSettings() {
const settings = UnknownUser.unknownUsers();
if (settings) {
settings.forEach(function updateSetting({ key, value }) {
this.settings[key] = value;
});
}
this.validateSettings();
}
validateSettings() {
if (
!this.settings.failuresBeforeLockout ||
this.settings.failuresBeforeLockout < 0
) {
throw new Error('"failuresBeforeLockout" is not positive integer');
}
if (
!this.settings.lockoutPeriod ||
this.settings.lockoutPeriod < 0
) {
throw new Error('"lockoutPeriod" is not positive integer');
}
if (
!this.settings.failureWindow ||
this.settings.failureWindow < 0
) {
throw new Error('"failureWindow" is not positive integer');
}
}
scheduleUnlocksForLockedAccounts() {
const lockedAccountsCursor = this.AccountsLockoutCollection.find(
{
'services.accounts-lockout.unlockTime': {
$gt: Number(new Date()),
},
},
{
fields: {
'services.accounts-lockout.unlockTime': 1,
},
},
);
const currentTime = Number(new Date());
lockedAccountsCursor.forEach((connection) => {
let lockDuration = this.unlockTime(connection) - currentTime;
if (lockDuration >= this.settings.lockoutPeriod) {
lockDuration = this.settings.lockoutPeriod * 1000;
}
if (lockDuration <= 1) {
lockDuration = 1;
}
Meteor.setTimeout(
this.unlockAccount.bind(this, connection.clientAddress),
lockDuration,
);
});
}
unlockAccountsIfLockoutAlreadyExpired() {
const currentTime = Number(new Date());
const query = {
'services.accounts-lockout.unlockTime': {
$lt: currentTime,
},
};
const data = {
$unset: {
'services.accounts-lockout.unlockTime': 0,
'services.accounts-lockout.failedAttempts': 0,
},
};
this.AccountsLockoutCollection.update(query, data);
}
hookIntoAccounts() {
Accounts.validateLoginAttempt(this.validateLoginAttempt.bind(this));
Accounts.onLogin(this.onLogin.bind(this));
}
validateLoginAttempt(loginInfo) {
// don't interrupt non-password logins
if (
loginInfo.type !== 'password' ||
loginInfo.user !== undefined ||
loginInfo.error === undefined ||
loginInfo.error.reason !== 'User not found'
) {
return loginInfo.allowed;
}
if (this.settings instanceof Function) {
this.settings = this.settings(loginInfo.connection);
this.validateSettings();
}
const clientAddress = loginInfo.connection.clientAddress;
const unlockTime = this.unlockTime(loginInfo.connection);
let failedAttempts = 1 + this.failedAttempts(loginInfo.connection);
const firstFailedAttempt = this.firstFailedAttempt(loginInfo.connection);
const currentTime = Number(new Date());
const canReset = (currentTime - firstFailedAttempt) > (1000 * this.settings.failureWindow);
if (canReset) {
failedAttempts = 1;
this.resetAttempts(failedAttempts, clientAddress);
}
const canIncrement = failedAttempts < this.settings.failuresBeforeLockout;
if (canIncrement) {
this.incrementAttempts(failedAttempts, clientAddress);
}
const maxAttemptsAllowed = this.settings.failuresBeforeLockout;
const attemptsRemaining = maxAttemptsAllowed - failedAttempts;
if (unlockTime > currentTime) {
let duration = unlockTime - currentTime;
duration = Math.ceil(duration / 1000);
duration = duration > 1 ? duration : 1;
UnknownUser.tooManyAttempts(duration);
}
if (failedAttempts === maxAttemptsAllowed) {
this.setNewUnlockTime(failedAttempts, clientAddress);
let duration = this.settings.lockoutPeriod;
duration = Math.ceil(duration);
duration = duration > 1 ? duration : 1;
return UnknownUser.tooManyAttempts(duration);
}
return UnknownUser.userNotFound(
failedAttempts,
maxAttemptsAllowed,
attemptsRemaining,
);
}
resetAttempts(
failedAttempts,
clientAddress,
) {
const currentTime = Number(new Date());
const query = { clientAddress };
const data = {
$set: {
'services.accounts-lockout.failedAttempts': failedAttempts,
'services.accounts-lockout.lastFailedAttempt': currentTime,
'services.accounts-lockout.firstFailedAttempt': currentTime,
},
};
this.AccountsLockoutCollection.upsert(query, data);
}
incrementAttempts(
failedAttempts,
clientAddress,
) {
const currentTime = Number(new Date());
const query = { clientAddress };
const data = {
$set: {
'services.accounts-lockout.failedAttempts': failedAttempts,
'services.accounts-lockout.lastFailedAttempt': currentTime,
},
};
this.AccountsLockoutCollection.upsert(query, data);
}
setNewUnlockTime(
failedAttempts,
clientAddress,
) {
const currentTime = Number(new Date());
const newUnlockTime = (1000 * this.settings.lockoutPeriod) + currentTime;
const query = { clientAddress };
const data = {
$set: {
'services.accounts-lockout.failedAttempts': failedAttempts,
'services.accounts-lockout.lastFailedAttempt': currentTime,
'services.accounts-lockout.unlockTime': newUnlockTime,
},
};
this.AccountsLockoutCollection.upsert(query, data);
Meteor.setTimeout(
this.unlockAccount.bind(this, clientAddress),
this.settings.lockoutPeriod * 1000,
);
}
onLogin(loginInfo) {
if (loginInfo.type !== 'password') {
return;
}
const clientAddress = loginInfo.connection.clientAddress;
const query = { clientAddress };
const data = {
$unset: {
'services.accounts-lockout.unlockTime': 0,
'services.accounts-lockout.failedAttempts': 0,
},
};
this.AccountsLockoutCollection.update(query, data);
}
static userNotFound(
failedAttempts,
maxAttemptsAllowed,
attemptsRemaining,
) {
throw new Meteor.Error(
403,
'User not found',
JSON.stringify({
message: 'User not found',
failedAttempts,
maxAttemptsAllowed,
attemptsRemaining,
}),
);
}
static tooManyAttempts(duration) {
throw new Meteor.Error(
403,
'Too many attempts',
JSON.stringify({
message: 'Wrong emails were submitted too many times. Account is locked for a while.',
duration,
}),
);
}
static unknownUsers() {
let unknownUsers;
try {
unknownUsers = Meteor.settings['accounts-lockout'].unknownUsers;
} catch (e) {
unknownUsers = false;
}
return unknownUsers || false;
}
findOneByConnection(connection) {
return this.AccountsLockoutCollection.findOne({
clientAddress: connection.clientAddress,
});
}
unlockTime(connection) {
connection = this.findOneByConnection(connection);
let unlockTime;
try {
unlockTime = connection.services['accounts-lockout'].unlockTime;
} catch (e) {
unlockTime = 0;
}
return unlockTime || 0;
}
failedAttempts(connection) {
connection = this.findOneByConnection(connection);
let failedAttempts;
try {
failedAttempts = connection.services['accounts-lockout'].failedAttempts;
} catch (e) {
failedAttempts = 0;
}
return failedAttempts || 0;
}
lastFailedAttempt(connection) {
connection = this.findOneByConnection(connection);
let lastFailedAttempt;
try {
lastFailedAttempt = connection.services['accounts-lockout'].lastFailedAttempt;
} catch (e) {
lastFailedAttempt = 0;
}
return lastFailedAttempt || 0;
}
firstFailedAttempt(connection) {
connection = this.findOneByConnection(connection);
let firstFailedAttempt;
try {
firstFailedAttempt = connection.services['accounts-lockout'].firstFailedAttempt;
} catch (e) {
firstFailedAttempt = 0;
}
return firstFailedAttempt || 0;
}
unlockAccount(clientAddress) {
const query = { clientAddress };
const data = {
$unset: {
'services.accounts-lockout.unlockTime': 0,
'services.accounts-lockout.failedAttempts': 0,
},
};
this.AccountsLockoutCollection.update(query, data);
}
}
export default UnknownUser;

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1,11 @@
wekan-cfs-base-package
=========================
This is a Meteor package used by
[CollectionFS](https://github.com/zcfs/Meteor-CollectionFS).
You don't need to manually add this package to your app. It is added when you
add the `wekan-cfs-standard-packages` package.
This package provides the `FS` namespace and helper methods used by many
CollectionFS packages.

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1,179 @@
/*
* Copyright (c) 2010 Nick Galbreath
* http://code.google.com/p/stringencoders/source/browse/#svn/trunk/javascript
*
* Permission is hereby granted, free of charge, to any person
* obtaining a copy of this software and associated documentation
* files (the "Software"), to deal in the Software without
* restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the
* Software is furnished to do so, subject to the following
* conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
* OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
* OTHER DEALINGS IN THE SOFTWARE.
*/
/* base64 encode/decode compatible with window.btoa/atob
*
* window.atob/btoa is a Firefox extension to convert binary data (the "b")
* to base64 (ascii, the "a").
*
* It is also found in Safari and Chrome. It is not available in IE.
*
* if (!window.btoa) window.btoa = base64.encode
* if (!window.atob) window.atob = base64.decode
*
* The original spec's for atob/btoa are a bit lacking
* https://developer.mozilla.org/en/DOM/window.atob
* https://developer.mozilla.org/en/DOM/window.btoa
*
* window.btoa and base64.encode takes a string where charCodeAt is [0,255]
* If any character is not [0,255], then an DOMException(5) is thrown.
*
* window.atob and base64.decode take a base64-encoded string
* If the input length is not a multiple of 4, or contains invalid characters
* then an DOMException(5) is thrown.
*/
var base64 = {};
base64.PADCHAR = '=';
base64.ALPHA = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
base64.makeDOMException = function() {
// sadly in FF,Safari,Chrome you can't make a DOMException
var e, tmp;
try {
return new DOMException(DOMException.INVALID_CHARACTER_ERR);
} catch (tmp) {
// not available, just passback a duck-typed equiv
// https://developer.mozilla.org/en/Core_JavaScript_1.5_Reference/Global_Objects/Error
// https://developer.mozilla.org/en/Core_JavaScript_1.5_Reference/Global_Objects/Error/prototype
var ex = new Error("DOM Exception 5");
// ex.number and ex.description is IE-specific.
ex.code = ex.number = 5;
ex.name = ex.description = "INVALID_CHARACTER_ERR";
// Safari/Chrome output format
ex.toString = function() { return 'Error: ' + ex.name + ': ' + ex.message; };
return ex;
}
}
base64.getbyte64 = function(s,i) {
// This is oddly fast, except on Chrome/V8.
// Minimal or no improvement in performance by using a
// object with properties mapping chars to value (eg. 'A': 0)
var idx = base64.ALPHA.indexOf(s.charAt(i));
if (idx === -1) {
throw base64.makeDOMException();
}
return idx;
}
base64.decode = function(s) {
// convert to string
s = '' + s;
var getbyte64 = base64.getbyte64;
var pads, i, b10;
var imax = s.length
if (imax === 0) {
return s;
}
if (imax % 4 !== 0) {
throw base64.makeDOMException();
}
pads = 0
if (s.charAt(imax - 1) === base64.PADCHAR) {
pads = 1;
if (s.charAt(imax - 2) === base64.PADCHAR) {
pads = 2;
}
// either way, we want to ignore this last block
imax -= 4;
}
var x = [];
for (i = 0; i < imax; i += 4) {
b10 = (getbyte64(s,i) << 18) | (getbyte64(s,i+1) << 12) |
(getbyte64(s,i+2) << 6) | getbyte64(s,i+3);
x.push(String.fromCharCode(b10 >> 16, (b10 >> 8) & 0xff, b10 & 0xff));
}
switch (pads) {
case 1:
b10 = (getbyte64(s,i) << 18) | (getbyte64(s,i+1) << 12) | (getbyte64(s,i+2) << 6);
x.push(String.fromCharCode(b10 >> 16, (b10 >> 8) & 0xff));
break;
case 2:
b10 = (getbyte64(s,i) << 18) | (getbyte64(s,i+1) << 12);
x.push(String.fromCharCode(b10 >> 16));
break;
}
return x.join('');
}
base64.getbyte = function(s,i) {
var x = s.charCodeAt(i);
if (x > 255) {
throw base64.makeDOMException();
}
return x;
}
base64.encode = function(s) {
if (arguments.length !== 1) {
throw new SyntaxError("Not enough arguments");
}
var padchar = base64.PADCHAR;
var alpha = base64.ALPHA;
var getbyte = base64.getbyte;
var i, b10;
var x = [];
// convert to string
s = '' + s;
var imax = s.length - s.length % 3;
if (s.length === 0) {
return s;
}
for (i = 0; i < imax; i += 3) {
b10 = (getbyte(s,i) << 16) | (getbyte(s,i+1) << 8) | getbyte(s,i+2);
x.push(alpha.charAt(b10 >> 18));
x.push(alpha.charAt((b10 >> 12) & 0x3F));
x.push(alpha.charAt((b10 >> 6) & 0x3f));
x.push(alpha.charAt(b10 & 0x3f));
}
switch (s.length - imax) {
case 1:
b10 = getbyte(s,i) << 16;
x.push(alpha.charAt(b10 >> 18) + alpha.charAt((b10 >> 12) & 0x3F) +
padchar + padchar);
break;
case 2:
b10 = (getbyte(s,i) << 16) | (getbyte(s,i+1) << 8);
x.push(alpha.charAt(b10 >> 18) + alpha.charAt((b10 >> 12) & 0x3F) +
alpha.charAt((b10 >> 6) & 0x3f) + padchar);
break;
}
return x.join('');
}
if (!window.btoa) window.btoa = base64.encode
if (!window.atob) window.atob = base64.decode

View file

@ -0,0 +1,161 @@
function equals(a, b) {
return EJSON.stringify(a) === EJSON.stringify(b);
}
Tinytest.add('cfs-base-package - test environment', function(test) {
test.isTrue(typeof FS !== 'undefined',
'FS scope not declared');
test.isTrue(typeof FS.Store !== 'undefined',
'FS scope "FS.Store" not declared');
test.isTrue(typeof FS.AccessPoint !== 'undefined',
'FS scope "FS.AccessPoint" not declared');
test.isTrue(typeof FS.Utility !== 'undefined',
'FS scope "FS.Utility" not declared');
test.isTrue(typeof FS._collections !== 'undefined',
'FS scope "FS._collections" not declared');
test.isTrue(typeof _Utility !== 'undefined',
'_Utility test scope not declared');
});
Tinytest.add('cfs-base-package - _Utility.defaultZero', function(test) {
test.equal(_Utility.defaultZero(), 0, 'Failes to return 0 when (undefined)');
test.equal(_Utility.defaultZero(undefined), 0, 'Failes to return 0 when undefined');
test.equal(_Utility.defaultZero(null), 0, 'Failes to return 0 when null');
test.equal(_Utility.defaultZero(false), 0, 'Failes to return 0 when false');
test.equal(_Utility.defaultZero(0), 0, 'Failes to return 0 when 0');
test.equal(_Utility.defaultZero(-1), -1, 'Failes to return -1');
test.equal(_Utility.defaultZero(1), 1, 'Failes to return 1');
test.equal(_Utility.defaultZero(-0.1), -0.1, 'Failes to return -0.1');
test.equal(_Utility.defaultZero(0.1), 0.1, 'Failes to return 0.1');
test.equal(_Utility.defaultZero(''), 0, 'Failes to return ""');
test.equal(_Utility.defaultZero({}), NaN, 'Failes to return NaN when object');
test.equal(_Utility.defaultZero("dfdsfs"), NaN, 'Failes to return NaN when string');
test.equal(_Utility.defaultZero("1"), 1, 'Failes to return 1 when string "1"');
});
Tinytest.add('cfs-base-package - FS.Utility.cloneFileRecord', function(test) {
// Given an object with any props, should filter out 'collectionName',
// 'collection', 'data', and 'createdByTransform'
var result = FS.Utility.cloneFileRecord({a: 1, b: {c: 1}, d: [1, 2], collectionName: 'test', collection: {}, data: {}, createdByTransform: false});
test.equal(result, {a: 1, b: {c: 1}, d: [1, 2]});
// Given an FS.File instance, should filter out 'collectionName',
// 'collection', 'data', and 'createdByTransform' and return a plain Object
var fileObj = new FS.File({a: 1, b: {c: 1}, d: [1, 2], name: 'name.png', type: 'image/png', size: 100, collectionName: 'test', collection: {}, data: {}, createdByTransform: false});
test.isTrue(fileObj instanceof FS.File);
var result = FS.Utility.cloneFileRecord(fileObj);
test.isFalse(result instanceof FS.File);
test.isTrue(equals(result, {a: 1, b: {c: 1}, d: [1, 2], name: 'name.png', type: 'image/png', size: 100}));
});
Tinytest.add('cfs-base-package - FS.Utility.defaultCallback', function(test) {
// should throw an error passed in, but not a Meteor.Error
test.throws(function () {
var cb = FS.Utility.defaultCallback;
cb(new Error('test'));
});
var cb2 = FS.Utility.defaultCallback;
test.isUndefined(cb2(new Meteor.Error('test')));
});
Tinytest.add('cfs-base-package - FS.Utility.handleError', function(test) {
test.isTrue(true);
// TODO
});
Tinytest.add('cfs-base-package - FS.Utility.binaryToBuffer', function(test) {
test.isTrue(true);
// TODO
});
Tinytest.add('cfs-base-package - FS.Utility.bufferToBinary', function(test) {
test.isTrue(true);
// TODO
});
Tinytest.add('cfs-base-package - FS.Utility.connectionLogin', function(test) {
test.isTrue(true);
// TODO
});
Tinytest.add('cfs-base-package - FS.Utility.getFileName', function(test) {
function t(input, expected) {
var ext = FS.Utility.getFileName(input);
test.equal(ext, expected, 'Got incorrect filename');
}
t('bar.png', 'bar.png');
t('foo/bar.png', 'bar.png');
t('/foo/foo/bar.png', 'bar.png');
t('http://foobar.com/file.png', 'file.png');
t('http://foobar.com/file', 'file');
t('http://foobar.com/file.png?a=b', 'file.png');
t('http://foobar.com/.file?a=b', '.file');
t('file', 'file');
t('.file', '.file');
t('foo/.file', '.file');
t('/foo/foo/.file', '.file');
});
Tinytest.add('cfs-base-package - FS.Utility.getFileExtension', function(test) {
function t(input, expected) {
var ext = FS.Utility.getFileExtension(input);
test.equal(ext, expected, 'Got incorrect extension');
}
t('bar.png', 'png');
t('foo/bar.png', 'png');
t('/foo/foo/bar.png', 'png');
t('http://foobar.com/file.png', 'png');
t('http://foobar.com/file', '');
t('http://foobar.com/file.png?a=b', 'png');
t('http://foobar.com/file?a=b', '');
t('file', '');
t('.file', '');
t('foo/.file', '');
t('/foo/foo/.file', '');
});
Tinytest.add('cfs-base-package - FS.Utility.setFileExtension', function(test) {
function t(name, ext, expected) {
var newName = FS.Utility.setFileExtension(name, ext);
test.equal(newName, expected, 'Extension was not set correctly');
}
t('bar.png', 'jpeg', 'bar.jpeg');
t('bar', 'jpeg', 'bar.jpeg');
t('.bar', 'jpeg', '.bar.jpeg');
t('', 'jpeg', '');
t(null, 'jpeg', null);
});
//Test API:
//Tinytest.add('', function(test) {});
//Tinytest.addAsync('', function(test, onComplete) {});
//test.isFalse(v, msg)
//test.isTrue(v, msg)
//test.equalactual, expected, message, not
//test.length(obj, len)
//test.include(s, v)
//test.isNaN(v, msg)
//test.isUndefined(v, msg)
//test.isNotNull
//test.isNull
//test.throws(func)
//test.instanceOf(obj, klass)
//test.notEqual(actual, expected, message)
//test.runId()
//test.exception(exception)
//test.expect_fail()
//test.ok(doc)
//test.fail(doc)
//test.equal(a, b, msg)

View file

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

View file

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

View file

@ -0,0 +1,8 @@
wekan-cfs-collection-filters
=========================
This is a Meteor package used by
[CollectionFS](https://github.com/zcfs/Meteor-CollectionFS).
You don't need to manually add this package to your app. It is added when you
add the `wekan-cfs-standard-packages` package.

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1,27 @@
function equals(a, b) {
return !!(EJSON.stringify(a) === EJSON.stringify(b));
}
Tinytest.add('cfs-collection-filters - client - test environment', function(test) {
test.isTrue(typeof FS.Collection !== 'undefined', 'test environment not initialized FS.Collection');
});
//Test API:
//test.isFalse(v, msg)
//test.isTrue(v, msg)
//test.equalactual, expected, message, not
//test.length(obj, len)
//test.include(s, v)
//test.isNaN(v, msg)
//test.isUndefined(v, msg)
//test.isNotNull
//test.isNull
//test.throws(func)
//test.instanceOf(obj, klass)
//test.notEqual(actual, expected, message)
//test.runId()
//test.exception(exception)
//test.expect_fail()
//test.ok(doc)
//test.fail(doc)
//test.equal(a, b, msg)

View file

@ -0,0 +1,27 @@
function equals(a, b) {
return !!(EJSON.stringify(a) === EJSON.stringify(b));
}
Tinytest.add('cfs-collection-filters - server - test environment', function(test) {
test.isTrue(typeof FS.Collection !== 'undefined', 'test environment not initialized FS.Collection');
});
//Test API:
//test.isFalse(v, msg)
//test.isTrue(v, msg)
//test.equalactual, expected, message, not
//test.length(obj, len)
//test.include(s, v)
//test.isNaN(v, msg)
//test.isUndefined(v, msg)
//test.isNotNull
//test.isNull
//test.throws(func)
//test.instanceOf(obj, klass)
//test.notEqual(actual, expected, message)
//test.runId()
//test.exception(exception)
//test.expect_fail()
//test.ok(doc)
//test.fail(doc)
//test.equal(a, b, msg)

View file

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

View file

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

View file

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

View file

@ -0,0 +1,8 @@
wekan-cfs-collection
=========================
This is a Meteor package used by
[CollectionFS](https://github.com/zcfs/Meteor-CollectionFS).
You don't need to manually add this package to your app. It is added when you
add the `wekan-cfs-standard-packages` package.

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1,33 @@
function equals(a, b) {
return !!(EJSON.stringify(a) === EJSON.stringify(b));
}
Tinytest.add('cfs-collection - test environment', function(test) {
test.isTrue(typeof FS.Collection !== 'undefined', 'test environment not initialized FS.Collection');
});
/*
* TODO FS.Collection Client Tests
*
*/
//Test API:
//test.isFalse(v, msg)
//test.isTrue(v, msg)
//test.equalactual, expected, message, not
//test.length(obj, len)
//test.include(s, v)
//test.isNaN(v, msg)
//test.isUndefined(v, msg)
//test.isNotNull
//test.isNull
//test.throws(func)
//test.instanceOf(obj, klass)
//test.notEqual(actual, expected, message)
//test.runId()
//test.exception(exception)
//test.expect_fail()
//test.ok(doc)
//test.fail(doc)
//test.equal(a, b, msg)

View file

@ -0,0 +1,96 @@
function equals(a, b) {
return !!(EJSON.stringify(a) === EJSON.stringify(b));
}
var fileCollection = new FS.Collection('files', {
stores: [
new FS.Store.GridFS('files')
]
});
Tinytest.add('cfs-collection - test environment', function(test) {
test.isTrue(typeof FS.Collection !== 'undefined', 'test environment not initialized FS.Collection');
});
Tinytest.add('cfs-collection - insert URL - sync - one', function (test) {
// XXX should switch to a local URL we host
// One
try {
fileCollection.insert("http://cdn.morguefile.com/imageData/public/files/b/bboomerindenial/preview/fldr_2009_04_01/file3301238617907.jpg");
test.isTrue(true);
} catch (err) {
test.isFalse(!!err, "URL insert failed");
}
});
Tinytest.add('cfs-collection - insert URL - sync - loop', function (test) {
// XXX should switch to a local URL we host
try {
for (var i = 0, len = 10; i < len; i++) {
fileCollection.insert("http://cdn.morguefile.com/imageData/public/files/b/bboomerindenial/preview/fldr_2009_04_01/file3301238617907.jpg");
}
test.isTrue(true);
} catch (err) {
test.isFalse(!!err, "URL insert failed");
}
});
Tinytest.addAsync('cfs-collection - insert URL - async - one', function (test, next) {
// XXX should switch to a local URL we host
// One
try {
fileCollection.insert("http://cdn.morguefile.com/imageData/public/files/b/bboomerindenial/preview/fldr_2009_04_01/file3301238617907.jpg", function (error, result) {
test.isNull(error);
test.instanceOf(result, FS.File);
next();
});
} catch (err) {
test.isFalse(!!err, "URL insert failed");
next();
}
});
Tinytest.addAsync('cfs-collection - insert URL - async - loop', function (test, next) {
// XXX should switch to a local URL we host
try {
var done = 0;
for (var i = 0, len = 10; i < len; i++) {
fileCollection.insert("http://cdn.morguefile.com/imageData/public/files/b/bboomerindenial/preview/fldr_2009_04_01/file3301238617907.jpg", function (error, result) {
test.isNull(error);
test.instanceOf(result, FS.File);
done++;
if (done === 10) {
next();
}
});
}
} catch (err) {
test.isFalse(!!err, "URL insert failed");
next();
}
});
//Test API:
//test.isFalse(v, msg)
//test.isTrue(v, msg)
//test.equalactual, expected, message, not
//test.length(obj, len)
//test.include(s, v)
//test.isNaN(v, msg)
//test.isUndefined(v, msg)
//test.isNotNull
//test.isNull
//test.throws(func)
//test.instanceOf(obj, klass)
//test.notEqual(actual, expected, message)
//test.runId()
//test.exception(exception)
//test.expect_fail()
//test.ok(doc)
//test.fail(doc)
//test.equal(a, b, msg)

View file

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

View file

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

View file

@ -0,0 +1,8 @@
wekan-cfs-data-man [![Build Status](https://travis-ci.org/CollectionFS/Meteor-data-man.svg?branch=master)](https://travis-ci.org/CollectionFS/Meteor-data-man)
=========================
Who can handle your arbitrary data? The DataMan can.
This is a package used by CollectionFS to attach data to file objects. Could be used for other things, though.
[Public API](api.md)

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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