Merge branch 'release/1.0.0' into production

This commit is contained in:
Raymond Feng 2014-04-04 13:36:12 -07:00
commit 50cd49dd9a
29 changed files with 2948 additions and 1022 deletions

506
LICENSE
View File

@ -1,19 +1,493 @@
Copyright (c) 2013 StrongLoop, Inc.
Copyright (c) 2013-2014 StrongLoop, Inc.
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:
loopback-storage-service uses a 'dual license' model. Users may use
loopback-storage-service under the terms of the Artistic 2.0 license, or under
the StrongLoop License. The text of both is included below.
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
Artistic License 2.0
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.
Copyright (c) 2000-2006, The Perl Foundation.
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
This license establishes the terms under which a given free software
Package may be copied, modified, distributed, and/or redistributed.
The intent is that the Copyright Holder maintains some artistic
control over the development of that Package while still keeping the
Package available as open source and free software.
You are always permitted to make arrangements wholly outside of this
license directly with the Copyright Holder of a given Package. If the
terms of this license do not permit the full use that you propose to
make of the Package, you should contact the Copyright Holder and seek
a different licensing arrangement.
Definitions
"Copyright Holder" means the individual(s) or organization(s)
named in the copyright notice for the entire Package.
"Contributor" means any party that has contributed code or other
material to the Package, in accordance with the Copyright Holder's
procedures.
"You" and "your" means any person who would like to copy,
distribute, or modify the Package.
"Package" means the collection of files distributed by the
Copyright Holder, and derivatives of that collection and/or of
those files. A given Package may consist of either the Standard
Version, or a Modified Version.
"Distribute" means providing a copy of the Package or making it
accessible to anyone else, or in the case of a company or
organization, to others outside of your company or organization.
"Distributor Fee" means any fee that you charge for Distributing
this Package or providing support for this Package to another
party. It does not mean licensing fees.
"Standard Version" refers to the Package if it has not been
modified, or has been modified only in ways explicitly requested
by the Copyright Holder.
"Modified Version" means the Package, if it has been changed, and
such changes were not explicitly requested by the Copyright
Holder.
"Original License" means this Artistic License as Distributed with
the Standard Version of the Package, in its current version or as
it may be modified by The Perl Foundation in the future.
"Source" form means the source code, documentation source, and
configuration files for the Package.
"Compiled" form means the compiled bytecode, object code, binary,
or any other form resulting from mechanical transformation or
translation of the Source form.
Permission for Use and Modification Without Distribution
(1) You are permitted to use the Standard Version and create and use
Modified Versions for any purpose without restriction, provided that
you do not Distribute the Modified Version.
Permissions for Redistribution of the Standard Version
(2) You may Distribute verbatim copies of the Source form of the
Standard Version of this Package in any medium without restriction,
either gratis or for a Distributor Fee, provided that you duplicate
all of the original copyright notices and associated disclaimers. At
your discretion, such verbatim copies may or may not include a
Compiled form of the Package.
(3) You may apply any bug fixes, portability changes, and other
modifications made available from the Copyright Holder. The resulting
Package will still be considered the Standard Version, and as such
will be subject to the Original License.
Distribution of Modified Versions of the Package as Source
(4) You may Distribute your Modified Version as Source (either gratis
or for a Distributor Fee, and with or without a Compiled form of the
Modified Version) provided that you clearly document how it differs
from the Standard Version, including, but not limited to, documenting
any non-standard features, executables, or modules, and provided that
you do at least ONE of the following:
(a) make the Modified Version available to the Copyright Holder
of the Standard Version, under the Original License, so that the
Copyright Holder may include your modifications in the Standard
Version.
(b) ensure that installation of your Modified Version does not
prevent the user installing or running the Standard Version. In
addition, the Modified Version must bear a name that is different
from the name of the Standard Version.
(c) allow anyone who receives a copy of the Modified Version to
make the Source form of the Modified Version available to others
under
(i) the Original License or
(ii) a license that permits the licensee to freely copy,
modify and redistribute the Modified Version using the same
licensing terms that apply to the copy that the licensee
received, and requires that the Source form of the Modified
Version, and of any works derived from it, be made freely
available in that license fees are prohibited but Distributor
Fees are allowed.
Distribution of Compiled Forms of the Standard Version
or Modified Versions without the Source
(5) You may Distribute Compiled forms of the Standard Version without
the Source, provided that you include complete instructions on how to
get the Source of the Standard Version. Such instructions must be
valid at the time of your distribution. If these instructions, at any
time while you are carrying out such distribution, become invalid, you
must provide new instructions on demand or cease further distribution.
If you provide valid instructions or cease distribution within thirty
days after you become aware that the instructions are invalid, then
you do not forfeit any of your rights under this license.
(6) You may Distribute a Modified Version in Compiled form without
the Source, provided that you comply with Section 4 with respect to
the Source of the Modified Version.
Aggregating or Linking the Package
(7) You may aggregate the Package (either the Standard Version or
Modified Version) with other packages and Distribute the resulting
aggregation provided that you do not charge a licensing fee for the
Package. Distributor Fees are permitted, and licensing fees for other
components in the aggregation are permitted. The terms of this license
apply to the use and Distribution of the Standard or Modified Versions
as included in the aggregation.
(8) You are permitted to link Modified and Standard Versions with
other works, to embed the Package in a larger work of your own, or to
build stand-alone binary or bytecode versions of applications that
include the Package, and Distribute the result without restriction,
provided the result does not expose a direct interface to the Package.
Items That are Not Considered Part of a Modified Version
(9) Works (including, but not limited to, modules and scripts) that
merely extend or make use of the Package, do not, by themselves, cause
the Package to be a Modified Version. In addition, such works are not
considered parts of the Package itself, and are not subject to the
terms of this license.
General Provisions
(10) Any use, modification, and distribution of the Standard or
Modified Versions is governed by this Artistic License. By using,
modifying or distributing the Package, you accept this license. Do not
use, modify, or distribute the Package, if you do not accept this
license.
(11) If your Modified Version has been derived from a Modified
Version made by someone other than you, you are nevertheless required
to ensure that your Modified Version complies with the requirements of
this license.
(12) This license does not grant you the right to use any trademark,
service mark, tradename, or logo of the Copyright Holder.
(13) This license includes the non-exclusive, worldwide,
free-of-charge patent license to make, have made, use, offer to sell,
sell, import and otherwise transfer the Package with respect to any
patent claims licensable by the Copyright Holder that are necessarily
infringed by the Package. If you institute patent litigation
(including a cross-claim or counterclaim) against any party alleging
that the Package constitutes direct or contributory patent
infringement, then this Artistic License to you shall terminate on the
date that such litigation is filed.
(14) Disclaimer of Warranty:
THE PACKAGE IS PROVIDED BY THE COPYRIGHT HOLDER AND CONTRIBUTORS "AS
IS' AND WITHOUT ANY EXPRESS OR IMPLIED WARRANTIES. THE IMPLIED
WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, OR
NON-INFRINGEMENT ARE DISCLAIMED TO THE EXTENT PERMITTED BY YOUR LOCAL
LAW. UNLESS REQUIRED BY LAW, NO COPYRIGHT HOLDER OR CONTRIBUTOR WILL
BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES ARISING IN ANY WAY OUT OF THE USE OF THE PACKAGE, EVEN IF
ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
StrongLoop License
STRONGLOOP SUBSCRIPTION AGREEMENT
PLEASE READ THIS AGREEMENT CAREFULLY BEFORE YOU AGREE TO THESE TERMS. IF YOU
ARE ACTING ON BEHALF OF AN ENTITY, THEN YOU REPRESENT THAT YOU HAVE THE
AUTHORITY TO ENTER INTO THIS AGREEMENT ON BEHALF OF THAT ENTITY. IF YOU DO NOT
AGREE TO THESE TERMS, YOU SHOULD NOT AGREE TO THE TERMS OF THIS AGREEMENT OR
INSTALL OR USE THE SOFTWARE.
This StrongLoop Subscription Agreement ("Agreement") is made by and between
StrongLoop, Inc. ("StrongLoop") with its principal place of business at 107 S.
B St, Suite 220, San Mateo, CA 94401 and the person or entity entering into this
Agreement ("Customer"). The effective date ("Effective Date") of this Agreement
is the date Customer agrees to these terms or installs or uses the Software (as
defined below). This Agreement applies to Customer's use of the Software but it
shall be superseded by any signed agreement between you and StrongLoop
concerning the Software.
1. Subscriptions and Licenses.
1.1 Subscriptions. StrongLoop offers five different subscription levels to its
customers, each as more particularly described on StrongLoop's website located
at www.strongloop.com (the "StrongLoop Site"): (1) Free; (2) Developer; (3)
Professional; (4) Gold; and (5) Platinum. The actual subscription level
applicable to Customer (the "Subscription") will be specified in the purchase
order that Customer issues to StrongLoop. This Agreement applies to Customer
regardless of the level of the Subscription selected by Customer and whether or
not Customer upgrades or downgrades its Subscription. StrongLoop hereby agrees
to provide the services as described on the StrongLoop Site for each
Subscription level during the term for which Customer has purchased the
applicable Subscription, subject to Customer paying the fees applicable to the
Subscription level purchased, if any (the "Subscription Fees"). StrongLoop may
modify the services to be provided under any Subscription upon notice to
Customer.
1.2 License Grant. Subject to the terms and conditions of this Agreement,
StrongLoop grants to Customer, during the Subscription Term (as defined in
Section 7.1 (Term and Termination) of this Agreement, a limited, non-exclusive,
non-transferable right and license, to install and use the StrongLoop Suite
software (the "Software") and the documentation made available electronically as
part of the Software (the "Documentation"), either of which may be modified
during the Term (as defined in Section 7.1 below), solely for development,
production and commercial purposes so long as Customer is using the Software to
run only one process on a given operating system at a time. This Agreement,
including but not limited to the license and restrictions contained herein,
apply to Customer regardless of whether Customer accesses the Software via
download from the StrongLoop Site or through a third-party website or service,
even if Customer acquired the Software prior to agreeing to this Agreement.
1.3 License Restrictions. Customer shall not itself, or through any parent,
subsidiary, affiliate, agent or other third party:
1.3.1 sell, lease, license, distribute, sublicense or otherwise transfer
in whole or in part, any Software or the Documentation to a third party;
or
1.3.2 decompile, disassemble, translate, reverse engineer or otherwise
attempt to derive source code from the Software, in whole or in part, nor
shall Customer use any mechanical, electronic or other method to trace,
decompile, disassemble, or identify the source code of the Software or
encourage others to do so, except to the limited extent, if any, that
applicable law permits such acts notwithstanding any contractual
prohibitions, provided, however, before Customer exercises any rights that
Customer believes to be entitled to based on mandatory law, Customer shall
provide StrongLoop with thirty (30) days prior written notice and provide
all reasonably requested information to allow StrongLoop to assess
Customer's claim and, at StrongLoop's sole discretion, to provide
alternatives that reduce any adverse impact on StrongLoop's intellectual
property or other rights; or
1.3.3 allow access or permit use of the Software by any users other than
Customer's employees or authorized third-party contractors who are
providing services to Customer and agree in writing to abide by the terms
of this Agreement, provided further that Customer shall be liable for any
failure by such employees and third-party contractors to comply with the
terms of this Agreement and no usage restrictions, if any, shall be
exceeded; or
1.3.4 create, develop, license, install, use, or deploy any third party
software or services to circumvent or provide access, permissions or
rights which violate the license keys embedded within the Software; or
1.3.5 modify or create derivative works based upon the Software or
Documentation; or disclose the results of any benchmark test of the
Software to any third party without StrongLoop's prior written approval;
or
1.3.6 change any proprietary rights notices which appear in the Software
or Documentation; or
1.3.7 use the Software as part of a time sharing or service bureau
purposes or in any other resale capacity.
1.4 Third-Party Software. The Software may include individual certain software
that is owned by third parties, including individual open source software
components (the "Third-Party Software"), each of which has its own copyright and
its own applicable license conditions. Such third-party software is licensed to
Customer under the terms of the applicable third-party licenses and/or copyright
notices that can be found in the LICENSES file, the Documentation or other
materials accompanying the Software, except that Sections 5 (Warranty
Disclaimer) and 6 (Limitation of Liability) also govern Customer's use of the
third-party software. Customer agrees to comply with the terms and conditions
of the relevant third-party software licenses.
2. Support Services. StrongLoop has no obligation to provide any support for
the Software other than the support services specifically described on the
StrongLoop Site for the Subscription level procured by Customer. However,
StrongLoop has endeavored to establish a community of users of the Software who
have provided their own feedback, hints and advice regarding their experiences
in using the Software. You can find that community and user feedback on the
StrongLoop Site. The use of any information, content or other materials from,
contained in or on the StrongLoop Site are subject to the StrongLoop website
terms of use located here http://www.strongloop.com/terms-of-service.
3. Confidentiality. For purposes of this Agreement, "Confidential Information"
means any and all information or proprietary materials (in every form and media)
not generally known in the relevant trade or industry and which has been or is
hereafter disclosed or made available by StrongLoop to Customer in connection
with the transactions contemplated under this Agreement, including (i) all trade
secrets, (ii) existing or contemplated Software, services, designs, technology,
processes, technical data, engineering, techniques, methodologies and concepts
and any related information, and (iii) information relating to business plans,
sales or marketing methods and customer lists or requirements. For a period of
five (5) years from the date of disclosure of the applicable Confidential
Information, Customer shall (i) hold the Confidential Information in trust and
confidence and avoid the disclosure or release thereof to any other person or
entity by using the same degree of care as it uses to avoid unauthorized use,
disclosure, or dissemination of its own Confidential Information of a similar
nature, but not less than reasonable care, and (ii) not use the Confidential
Information for any purpose whatsoever except as expressly contemplated under
this Agreement; provided that, to the extent the Confidential Information
constitutes a trade secret under law, Customer agrees to protect such
information for so long as it qualifies as a trade secret under applicable law.
Customer shall disclose the Confidential Information only to those of its
employees and contractors having a need to know such Confidential Information
and shall take all reasonable precautions to ensure that such employees and
contractors comply with the provisions of this Section. The obligations of
Customer under this Section shall not apply to information that Customer can
demonstrate (i) was in its possession at the time of disclosure and without
restriction as to confidentiality, (ii) at the time of disclosure is generally
available to the public or after disclosure becomes generally available to the
public through no breach of agreement or other wrongful act by Customer, (iii)
has been received from a third party without restriction on disclosure and
without breach of agreement by Customer, or (iv) is independently developed by
Customer without regard to the Confidential Information. In addition, Customer
may disclose Confidential Information as required to comply with binding orders
of governmental entities that have jurisdiction over it; provided that Customer
gives StrongLoop reasonable written notice to allow StrongLoop to seek a
protective order or other appropriate remedy, discloses only such Confidential
Information as is required by the governmental entity, and uses commercially
reasonable efforts to obtain confidential treatment for any Confidential
Information disclosed. Notwithstanding the above, Customer agrees that
StrongLoop, its employees and agents shall be free to use and employ their
general skills, know-how, and expertise, and to use, disclose, and employ any
generalized ideas, concepts, know-how, methods, techniques or skills gained or
learned during the Term or thereafter.
4. Ownership. StrongLoop shall retain all intellectual property and proprietary
rights in the Software, Documentation, and related works, including but not
limited to any derivative work of the foregoing and StrongLoop's licensors shall
retain all intellectual property and proprietary rights in any Third-Party
Software that may be provided with or as a part of the Software. Customer shall
do nothing inconsistent with StrongLoop's or its licensors' title to the
Software and the intellectual property rights embodied therein, including, but
not limited to, transferring, loaning, selling, assigning, pledging, or
otherwise disposing, encumbering, or suffering a lien or encumbrance upon or
against any interest in the Software. The Software (including any Third-Party
Software) contain copyrighted material, trade secrets and other proprietary
material of StrongLoop and/or its licensors.
5. Warranty Disclaimer. THE SOFTWARE (INCLUDING ANY THIRD-PARTY SOFTWARE) AND
DOCUMENTATION MADE AVAILABLE TO CUSTOMER ARE PROVIDED "AS-IS" AND STRONGLOOP,
ON BEHALF OF ITSELF AND ITS LICENSORS, EXPRESSLY DISCLAIMS ALL WARRANTIES OF ANY
KIND, EXPRESS OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, ANY IMPLIED WARRANTIES
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, NON-INFRINGEMENT, TITLE,
PERFORMANCE, AND ACCURACY AND ANY IMPLIED WARRANTIES ARISING FROM STATUTE,
COURSE OF DEALING, COURSE OF PERFORMANCE, OR USAGE OF TRADE. STRONGLOOP DOES
NOT WARRANT THAT THE OPERATION OF THE SOFTWARE WILL BE UNINTERRUPTED OR
ERROR-FREE, THAT DEFECTS IN THE SOFTWARE WILL BE CORRECTED OR THAT THE SOFTWARE
WILL PROVIDE OR ENSURE ANY PARTICULAR RESULTS OR OUTCOME. NO ORAL OR WRITTEN
INFORMATION OR ADVICE GIVEN BY STRONGLOOP OR ITS AUTHORIZED REPRESENTATIVES
SHALL CREATE A WARRANTY OR IN ANY WAY INCREASE THE SCOPE OF THIS WARRANTY.
STRONGLOOP IS NOT OBLIGATED TO PROVIDE CUSTOMER WITH UPGRADES TO THE SOFTWARE,
BUT MAY ELECT TO DO SO IN ITS SOLE DISCRETION. SOME JURISDICTIONS DO NOT ALLOW
THE EXCLUSION OF IMPLIED WARRANTIES, SO THE ABOVE EXCLUSION MAY NOT APPLY TO
CUSTOMER.WITHOUT LIMITING THE GENERALITY OF THE FOREGOING DISCLAIMER, THE
SOFTWARE AND DOCUMENTATION ARE NOT DESIGNED, MANUFACTURED OR INTENDED FOR USE IN
THE PLANNING, CONSTRUCTION, MAINTENANCE, CONTROL, OR DIRECT OPERATION OF NUCLEAR
FACILITIES, AIRCRAFT NAVIGATION, CONTROL OR COMMUNICATION SYSTEMS, WEAPONS
SYSTEMS, OR DIRECT LIFE SUPPORT SYSTEMS.
6. Limitation of Liability.
6.1 Exclusion of Liability. IN NO EVENT WILL STRONGLOOP OR ITS LICENSORS
BE LIABLE UNDER THIS AGREEMENT FOR ANY INDIRECT, RELIANCE, PUNITIVE,
CONSEQUENTIAL, SPECIAL, EXEMPLARY, OR INCIDENTAL DAMAGES OF ANY KIND AND
HOWEVER CAUSED (INCLUDING, WITHOUT LIMITATION, DAMAGES FOR LOSS OF
BUSINESS PROFITS, BUSINESS INTERRUPTION, LOSS OF BUSINESS INFORMATION AND
THE LIKE), EVEN IF STRONGLOOP HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH
DAMAGES. CUSTOMER BEARS FULL RESPONSIBILITY FOR USE OF THE SOFTWARE AND
THE SUBSCRIPTION AND STRONGLOOP DOES NOT GUARANTEE THAT THE USE OF THE
SOFTWARE AND SUBSCRIPTION WILL ENSURE THAT CUSTOMER'S NETWORK WILL BE
AVAILABLE, SECURE, MONITORED OR PROTECTED AGAINST ANY DOWNTIME, DENIAL OF
SERVICE ATTACKS, SECUITY BREACHES, HACKERS AND THE LIKE. IN NO EVENT WILL
STRONGLOOP'S CUMULATIVE LIABILITY FOR ANY DAMAGES, LOSSES AND CAUSES OF
ACTION (WHETHER IN CONTRACT, TORT, INCLUDING NEGLIGENCE, OR OTHERWISE)
ARISING OUT OF OR RELATED TO THIS AGREEMENT EXCEED THE GREATER OF ONE
HUNDRED DOLLARS (US$100) OR THE TOTAL SUBSCRIPTION FEES PAID BY CUSTOMER
TO STRONGLOOP IN THE TWELVE (12) MONTHS PRECEDING THE DATE THE CLAIM
ARISES.
6.2 Limitation of Damages. IN NO EVENT WILL STRONGLOOP'S LICENSORS HAVE
ANY LIABILITY FOR ANY CLAIM ARISING IN CONNECTION WITH THIS AGREEMENT.
THE PROVISIONS OF THIS SECTION 6 ALLOCATE RISKS UNDER THIS AGREEMENT
BETWEEN CUSTOMER, STRONGLOOP AND STRONGLOOP'S SUPPLIERS. THE FOREGOING
LIMITATIONS, EXCLUSIONS AND DISCLAIMERS APPLY TO THE MAXIMUM EXTENT
PERMITTED BY APPLICABLE LAW, EVEN IF ANY REMEDY FAILS IN ITS ESSENTIAL
PURPOSE.
6.3 Failure of Essential Purpose. THE PARTIES AGREE THAT THESE
LIMITATIONS SHALL APPLY EVEN IF THIS AGREEMENT OR ANY LIMITED REMEDY
SPECIFIED HEREIN IS FOUND TO HAVE FAILED OF ITS ESSENTIAL PURPOSE.
6.4 Allocation of Risk. The sections on limitation of liability and
disclaimer of warranties allocate the risks in the Agreement between the
parties. This allocation is an essential element of the basis of the
bargain between the parties.
7. Term and Termination.
7.1 This Agreement shall commence on the Effective Date and continue for so long
as Customer has a valid Subscription and is current on the payment of any
Subscription Fees required to be paid for that Subscription (the "Subscription
Term"). Either party may terminate this Agreement immediately upon written
notice to the other party, and the Subscription and licenses granted hereunder
automatically terminate upon the termination of this Agreement. This Agreement
will terminate immediately without notice from StrongLoop if Customer fails to
comply with or otherwise breaches any provision of this Agreement.
7.2 All Sections other than Section 1.1 (Subscriptions) and 1.2 (Licenses) shall
survive the expiration or termination of this Agreement.
8. Subscription Fees and Payments. StrongLoop, Customer agrees to pay
StrongLoop the Subscription Fees as described on the StrongLoop Site for the
Subscription purchased unless a different amount has been agreed to in a
separate agreement between Customer and StrongLoop. In addition, Customer shall
pay all sales, use, value added, withholding, excise taxes and other tax, duty,
custom and similar fees levied upon the delivery or use of the Software and the
Subscriptions described in this Agreement. Fees shall be invoiced in full upon
StrongLoop's acceptance of Customer's purchase order for the Subscription. All
invoices shall be paid in US dollars and are due upon receipt and shall be paid
within thirty (30) days. Payments shall be made without right of set-off or
chargeback. If Customer does not pay the invoices when due, StrongLoop may
charge interest at one percent (1%) per month or the highest rate permitted by
law, whichever is lower, on the unpaid balance from the original due date. If
Customer fails to pay fees in accordance with this Section, StrongLoop may
suspend fulfilling its obligations under this Agreement (including but not
limited to suspending the services under the Subscription) until payment is
received by StrongLoop. If any applicable law requires Customer to withhold
amounts from any payments to StrongLoop under this Agreement, (a) Customer shall
effect such withholding, remit such amounts to the appropriate taxing
authorities and promptly furnish StrongLoop with tax receipts evidencing the
payments of such amounts and (b) the sum payable by Customer upon which the
deduction or withholding is based shall be increased to the extent necessary to
ensure that, after such deduction or withholding, StrongLoop receives and
retains, free from liability for such deduction or withholding, a net amount
equal to the amount StrongLoop would have received and retained absent the
required deduction or withholding.
9. General.
9.1 Compliance with Laws. Customer shall abide by all local, state, federal and
international laws, rules, regulations and orders applying to Customer's use of
the Software, including, without limitation, the laws and regulations of the
United States that may restrict the export and re-export of certain commodities
and technical data of United States origin, including the Software. Customer
agrees that it will not export or re-export the Software without the appropriate
United States or foreign government licenses.
9.2 Entire Agreement. This Agreement constitutes the entire agreement between
the parties concerning the subject matter hereof. This Agreement supersedes all
prior or contemporaneous discussions, proposals and agreements between the
parties relating to the subject matter hereof. No amendment, modification or
waiver of any provision of this Agreement shall be effective unless in writing
and signed by both parties. Any additional or different terms on any purchase
orders issued by Customer to StrongLoop shall not be binding on either party,
are hereby rejected by StrongLoop and void.
9.3 Severability. If any provision of this Agreement is held to be invalid or
unenforceable, the remaining portions shall remain in full force and effect and
such provision shall be enforced to the maximum extent possible so as to effect
the intent of the parties and shall be reformed to the extent necessary to make
such provision valid and enforceable.
9.4 Waiver. No waiver of rights by either party may be implied from any actions
or failures to enforce rights under this Agreement.
9.5 Force Majeure. Neither party shall be liable to the other for any delay or
failure to perform due to causes beyond its reasonable control (excluding
payment of monies due).
9.6 No Third Party Beneficiaries. Unless otherwise specifically stated, the
terms of this Agreement are intended to be and are solely for the benefit of
StrongLoop and Customer and do not create any right in favor of any third party.
9.7 Governing Law and Jurisdiction. This Agreement shall be governed by the
laws of the State of California, without reference to the principles of
conflicts of law. The provisions of the Uniform Computerized Information
Transaction Act and United Nations Convention on Contracts for the International
Sale of Goods shall not apply to this Agreement. The parties shall attempt to
resolve any dispute related to this Agreement informally, initially through
their respective management, and then by non-binding mediation in San Francisco
County, California. Any litigation related to this Agreement shall be brought
in the state or federal courts located in San Francisco County, California, and
only in those courts and each party irrevocably waives any objections to such
venue.
9.8 Notices. All notices must be in writing and shall be effective three (3)
days after the date sent to the other party's headquarters, Attention Chief
Financial Officer.

154
README.md
View File

@ -1,27 +1,60 @@
# loopback-storage-service
LoopBack Storage Service
LoopBack storage service provides Node.js and REST APIs to manage binary contents
using pluggable storage providers, such as local file systems, Amazon S3, or
Rackspace cloud files. We use [pkgcloud](https://github.com/pkgcloud/pkgcloud) to support the cloud based
storage services including:
## Storage
- Amazon
- Rackspace
- Openstack
- Azure
The `loopback-storage-service` module is designed to make it easy to upload and download files to various infrastructure providers.
The binary artifacts are organized with containers and files. A container is the
collection of files. Each file will belong to a container.
To get started with a `loopback-storage-service` provider just create one:
## Define a model with the loopback-storage-service connector
``` js
var storageService = require('loopback-storage-service')({
//
// The name of the provider (e.g. "file")
//
provider: 'provider-name',
//
// ... Provider specific credentials
//
});
```
LoopBack exposes the APIs using a model that is attached to a data source configured
with the loopback-storage-service connector.
Each compute provider takes different credentials to authenticate; these details about each specific provider can be found below:
var ds = loopback.createDataSource({
connector: require('loopback-storage-service'),
provider: 'filesystem',
root: path.join(__dirname, 'storage')
});
var container = ds.createModel('container');
The following methods are mixed into the model class:
- getContainers(cb): List all containers
- createContainer(options, cb): Create a new container
- destroyContainer(container, cb): Destroy an existing container
- getContainer(container, cb): Look up a container by name
- uploadStream(container, file, options, cb): Get the stream for uploading
- downloadStream(container, file, options, cb): Get the stream for downloading
- getFiles(container, download, cb): List all files within the given container
- getFile(container, file, cb): Look up a file by name within the given container
- removeFile(container, file, cb): Remove a file by name within the given container
- upload(req, res, cb): Handle the file upload at the server side
- download(container, file, res, cb): Handle the file download at the server side
## Configure the storage providers
Each storage provider takes different settings; these details about each specific
provider can be found below:
* Local File System
{
provider: 'filesystem',
root: '/tmp/storage'
}
* Amazon
@ -41,53 +74,62 @@ Each compute provider takes different credentials to authenticate; these details
apiKey: '...'
}
* Azure
* Local File System
* OpenStack
{
provider: 'filesystem',
root: '/tmp/storage'
provider: 'openstack',
username: 'your-user-name',
password: 'your-password',
authUrl: 'https://your-identity-service'
}
Each instance of `storage.Client` returned from `storage.createClient` has a set of uniform APIs:
* Azure
### Container
* `storageService.getContainers(function (err, containers) { })`
* `storageService.createContainer(options, function (err, container) { })`
* `storageService.destroyContainer(containerName, function (err) { })`
* `storageService.getContainer(containerName, function (err, container) { })`
### File
* `storageService.upload(options, function (err) { })`
* `storageService.download(options, function (err) { })`
* `storageService.getFiles(container, function (err, files) { })`
* `storageService.getFile(container, file, function (err, server) { })`
* `storageService.removeFile(container, file, function (err) { })`
{
provider: 'azure',
storageAccount: "test-storage-account", // Name of your storage account
storageAccessKey: "test-storage-access-key" // Access key for storage account
}
Both the `.upload(options)` and `.download(options)` have had **careful attention paid to make sure they are pipe and stream capable:**
### Upload a File
``` js
var storage = require('loopback-storage-service'),
fs = require('fs');
var storageService = storage({ /* ... */ });
fs.createReadStream('a-file.txt').pipe(storageService.uploadStream('a-container','remote-file-name.txt'));
```
## REST APIs
### Download a File
``` js
var storage = require('loopback-storage-service'),
fs = require('fs');
var storageService = storage({ /* ... */ });
storageService.downloadStream({
container: 'a-container',
remote: 'remote-file-name.txt'
}).pipe(fs.createWriteStream('a-file.txt'));
```
- GET /api/containers
List all containers
- GET /api/containers/:container
Get information about a container by name
- POST /api/containers
Create a new container
- DELETE /api/containers/:container
Delete an existing container by name
- GET /api/containers/:container/files
List all files within a given container by name
- GET /api/containers/:container/files/:file
Get information for a file within a given container by name
- DELETE /api/containers/:container/files/:file
Delete a file within a given container by name
- POST /api/containers/:container/upload
Upload one or more files into the given container by name. The request body should
use [multipart/form-data](https://www.ietf.org/rfc/rfc2388.txt) which the file input
type for HTML uses.
- GET /api/containers/:container/download/:file
Download a file within a given container by name

8
docs.json Normal file
View File

@ -0,0 +1,8 @@
{
"content": [
{ "title": "LoopBack Storage Service", "depth": 2 },
"lib/storage-service.js",
{ "title": "Storage Handler API", "depth": 3 },
"lib/storage-handler.js"
]
}

1
example/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
providers-private.json

57
example/app-cloud.js Normal file
View File

@ -0,0 +1,57 @@
var StorageService = require('../').StorageService;
var path = require('path');
var providers = null;
try {
providers = require('./providers-private.json');
} catch(err) {
providers = require('./providers.json');
}
function listContainersAndFiles(ss) {
ss.getContainers(function (err, containers) {
if (err) {
console.error(err);
return;
}
console.log('----------- %s (%d) ---------------', ss.provider, containers.length);
containers.forEach(function (c) {
console.log('[%s] %s/', ss.provider, c.name);
c.getFiles(function (err, files) {
files.forEach(function (f) {
console.log('[%s] ... %s', ss.provider, f.name);
});
});
});
});
}
var rs = new StorageService({
provider: 'rackspace',
username: providers.rackspace.username,
apiKey: providers.rackspace.apiKey,
region: providers.rackspace.region
});
listContainersAndFiles(rs);
var s3 = new StorageService({
provider: 'amazon',
key: providers.amazon.key,
keyId: providers.amazon.keyId
});
listContainersAndFiles(s3);
var fs = require('fs');
var path = require('path');
var stream = s3.uploadStream('con1', 'test.jpg');
fs.createReadStream(path.join(__dirname, 'test.jpg')).pipe(stream);
var local = StorageService({
provider: 'filesystem',
root: path.join(__dirname, 'storage')
});
listContainersAndFiles(local);

View File

@ -1,51 +0,0 @@
var loopback = require('loopback')
, app = module.exports = loopback();
// var StorageService = require('../');
// expose a rest api
app.use(loopback.rest());
app.configure(function () {
app.set('port', process.env.PORT || 3000);
});
var ds = loopback.createDataSource({
connector: require('../lib/storage-connector'),
provider: 'filesystem',
root: '/tmp/storage'
});
var Container = ds.createModel('container', {name: String});
console.log(Container);
Container.getContainers(console.log);
console.log('shared', Container.getContainers.shared);
app.model(Container);
/*
var handler = new StorageService({provider: 'filesystem', root: '/tmp/storage'});
app.service('storage', handler);
app.get('/', function (req, res, next) {
res.setHeader('Content-Type', 'text/html');
var form = "<html><body><h1>Storage Service Demo</h1>" +
"<a href='/download'>List all containers</a><p>" +
"Upload to container c1: <p>" +
"<form method='POST' enctype='multipart/form-data' action='/upload/c1'>"
+ "File to upload: <input type=file name=uploadedFiles multiple=true><br>"
+ "Notes about the file: <input type=text name=note><br>"
+ "<input type=submit value=Upload></form>" +
"</body></html>";
res.send(form);
res.end();
});
*/
app.listen(app.get('port'));
console.log('http://127.0.0.1:' + app.get('port'));

View File

@ -1,91 +1,45 @@
var StorageService = require('../');
var loopback = require('loopback')
, app = module.exports = loopback();
var path = require('path');
var rs = StorageService({
provider: 'rackspace',
username: 'strongloop',
apiKey: 'your-rackspace-api-key'
app.use(app.router);
// expose a rest api
app.use('/api', loopback.rest());
app.use(loopback.static(path.join(__dirname, 'public')));
app.configure(function () {
app.set('port', process.env.PORT || 3000);
});
// Container
rs.getContainers(function (err, containers) {
if (err) {
console.error(err);
return;
}
containers.forEach(function (c) {
console.log('rackspace: ', c.name);
c.getFiles(function (err, files) {
files.forEach(function (f) {
console.log('....', f.name);
});
});
});
var ds = loopback.createDataSource({
connector: require('../index'),
provider: 'filesystem',
root: path.join(__dirname, 'storage')
});
var container = ds.createModel('container');
app.model(container);
/*
client.createContainer(options, function (err, container) { });
client.destroyContainer(containerName, function (err) { });
client.getContainer(containerName, function (err, container) { });
// File
client.upload(options, function (err) { });
client.download(options, function (err) { });
client.getFiles(container, function (err, files) { });
client.getFile(container, file, function (err, server) { });
client.removeFile(container, file, function (err) { });
*/
var s3 = StorageService({
provider: 'amazon',
key: 'your-amazon-key',
keyId: 'your-amazon-key-id'
});
s3.getContainers(function (err, containers) {
if (err) {
console.error(err);
return;
}
containers.forEach(function (c) {
console.log('amazon: ', c.name);
c.getFiles(function (err, files) {
files.forEach(function (f) {
console.log('....', f.name);
});
});
});
});
var fs = require('fs');
var path = require('path');
var stream = s3.uploadStream('con1','test.jpg');
var input = fs.createReadStream(path.join(__dirname, 'test.jpg')).pipe(stream);
var local = StorageService({
provider: 'filesystem',
root: path.join(__dirname, 'storage')
});
// Container
local.getContainers(function (err, containers) {
if (err) {
console.error(err);
return;
}
containers.forEach(function (c) {
console.log('filesystem: ', c.name);
c.getFiles(function (err, files) {
files.forEach(function (f) {
console.log('....', f.name);
});
});
});
app.get('/', function (req, res, next) {
res.setHeader('Content-Type', 'text/html');
var form = "<html><body><h1>Storage Service Demo</h1>" +
"<a href='/api/containers'>List all containers</a><p>" +
"Upload to container c1: <p>" +
"<form method='POST' enctype='multipart/form-data' action='/containers/container1/upload'>"
+ "File to upload: <input type=file name=uploadedFiles multiple=true><br>"
+ "Notes about the file: <input type=text name=note><br>"
+ "<input type=submit value=Upload></form>" +
"</body></html>";
res.send(form);
res.end();
});
*/
app.listen(app.get('port'));
console.log('http://127.0.0.1:' + app.get('port'));

11
example/providers.json Normal file
View File

@ -0,0 +1,11 @@
{
"rackspace": {
"username": "your-rackspace-username",
"apiKey": "your-rackspace-api-key",
"region": "DFW"
},
"amazon": {
"key": "your-amazon-key",
"keyId": "your-amazon-key-id"
}
}

685
example/public/angular-file-upload.js vendored Normal file
View File

@ -0,0 +1,685 @@
/*
Angular File Upload v0.3.3.1
https://github.com/nervgh/angular-file-upload
*/
(function(angular, factory) {
if (typeof define === 'function' && define.amd) {
define('angular-file-upload', ['angular'], function(angular) {
return factory(angular);
});
} else {
return factory(angular);
}
}(angular || null, function(angular) {
var app = angular.module('angularFileUpload', []);
// It is attached to an element that catches the event drop file
app.directive('ngFileDrop', [ '$fileUploader', function ($fileUploader) {
'use strict';
return {
// don't use drag-n-drop files in IE9, because not File API support
link: !$fileUploader.isHTML5 ? angular.noop : function (scope, element, attributes) {
element
.bind('drop', function (event) {
var dataTransfer = event.dataTransfer ?
event.dataTransfer :
event.originalEvent.dataTransfer; // jQuery fix;
if (!dataTransfer) return;
event.preventDefault();
event.stopPropagation();
scope.$broadcast('file:removeoverclass');
scope.$emit('file:add', dataTransfer.files, scope.$eval(attributes.ngFileDrop));
})
.bind('dragover', function (event) {
var dataTransfer = event.dataTransfer ?
event.dataTransfer :
event.originalEvent.dataTransfer; // jQuery fix;
event.preventDefault();
event.stopPropagation();
dataTransfer.dropEffect = 'copy';
scope.$broadcast('file:addoverclass');
})
.bind('dragleave', function () {
scope.$broadcast('file:removeoverclass');
});
}
};
}])
// It is attached to an element which will be assigned to a class "ng-file-over" or ng-file-over="className"
app.directive('ngFileOver', function () {
'use strict';
return {
link: function (scope, element, attributes) {
scope.$on('file:addoverclass', function () {
element.addClass(attributes.ngFileOver || 'ng-file-over');
});
scope.$on('file:removeoverclass', function () {
element.removeClass(attributes.ngFileOver || 'ng-file-over');
});
}
};
});
// It is attached to <input type="file"> element like <ng-file-select="options">
app.directive('ngFileSelect', [ '$fileUploader', function ($fileUploader) {
'use strict';
return {
link: function (scope, element, attributes) {
$fileUploader.isHTML5 || element.removeAttr('multiple');
element.bind('change', function () {
scope.$emit('file:add', $fileUploader.isHTML5 ? this.files : this, scope.$eval(attributes.ngFileSelect));
($fileUploader.isHTML5 && element.attr('multiple')) && element.prop('value', null);
});
element.prop('value', null); // FF fix
}
};
}]);
app.factory('$fileUploader', [ '$compile', '$rootScope', '$http', '$window', function ($compile, $rootScope, $http, $window) {
'use strict';
/**
* Creates a uploader
* @param {Object} params
* @constructor
*/
function Uploader(params) {
angular.extend(this, {
scope: $rootScope,
url: '/',
alias: 'file',
queue: [],
headers: {},
progress: null,
autoUpload: false,
removeAfterUpload: false,
method: 'POST',
filters: [],
formData: [],
isUploading: false,
_nextIndex: 0,
_timestamp: Date.now()
}, params);
// add the base filter
this.filters.unshift(this._filter);
this.scope.$on('file:add', function (event, items, options) {
event.stopPropagation();
this.addToQueue(items, options);
}.bind(this));
this.bind('beforeupload', Item.prototype._beforeupload);
this.bind('in:progress', Item.prototype._progress);
this.bind('in:success', Item.prototype._success);
this.bind('in:cancel', Item.prototype._cancel);
this.bind('in:error', Item.prototype._error);
this.bind('in:complete', Item.prototype._complete);
this.bind('in:progress', this._progress);
this.bind('in:complete', this._complete);
}
Uploader.prototype = {
/**
* Link to the constructor
*/
constructor: Uploader,
/**
* The base filter. If returns "true" an item will be added to the queue
* @param {File|Input} item
* @returns {boolean}
* @private
*/
_filter: function (item) {
return angular.isElement(item) ? true : !!item.size;
},
/**
* Registers a event handler
* @param {String} event
* @param {Function} handler
* @return {Function} unsubscribe function
*/
bind: function (event, handler) {
return this.scope.$on(this._timestamp + ':' + event, handler.bind(this));
},
/**
* Triggers events
* @param {String} event
* @param {...*} [some]
*/
trigger: function (event, some) {
arguments[ 0 ] = this._timestamp + ':' + event;
this.scope.$broadcast.apply(this.scope, arguments);
},
/**
* Checks a support the html5 uploader
* @returns {Boolean}
* @readonly
*/
isHTML5: !!($window.File && $window.FormData),
/**
* Adds items to the queue
* @param {FileList|File|HTMLInputElement} items
* @param {Object} [options]
*/
addToQueue: function (items, options) {
var length = this.queue.length;
var list = 'length' in items ? items : [items];
angular.forEach(list, function (file) {
// check a [File|HTMLInputElement]
var isValid = !this.filters.length ? true : this.filters.every(function (filter) {
return filter.call(this, file);
}, this);
// create new item
var item = new Item(angular.extend({
url: this.url,
alias: this.alias,
headers: angular.copy(this.headers),
formData: angular.copy(this.formData),
removeAfterUpload: this.removeAfterUpload,
method: this.method,
uploader: this,
file: file
}, options));
if (isValid) {
this.queue.push(item);
this.trigger('afteraddingfile', item);
} else {
this.trigger('whenaddingfilefailed', item);
}
}, this);
if (this.queue.length !== length) {
this.trigger('afteraddingall', this.queue);
this.progress = this._getTotalProgress();
}
this._render();
this.autoUpload && this.uploadAll();
},
/**
* Remove items from the queue. Remove last: index = -1
* @param {Item|Number} value
*/
removeFromQueue: function (value) {
var index = this.getIndexOfItem(value);
var item = this.queue[ index ];
item.isUploading && item.cancel();
this.queue.splice(index, 1);
item._destroy();
this.progress = this._getTotalProgress();
},
/**
* Clears the queue
*/
clearQueue: function () {
this.queue.forEach(function (item) {
item.isUploading && item.cancel();
item._destroy();
}, this);
this.queue.length = 0;
this.progress = 0;
},
/**
* Returns a index of item from the queue
* @param {Item|Number} value
* @returns {Number}
*/
getIndexOfItem: function (value) {
return angular.isObject(value) ? this.queue.indexOf(value) : value;
},
/**
* Returns not uploaded items
* @returns {Array}
*/
getNotUploadedItems: function () {
return this.queue.filter(function (item) {
return !item.isUploaded;
});
},
/**
* Returns items ready for upload
* @returns {Array}
*/
getReadyItems: function() {
return this.queue
.filter(function(item) {
return item.isReady && !item.isUploading;
})
.sort(function(item1, item2) {
return item1.index - item2.index;
});
},
/**
* Uploads a item from the queue
* @param {Item|Number} value
*/
uploadItem: function (value) {
var index = this.getIndexOfItem(value);
var item = this.queue[ index ];
var transport = this.isHTML5 ? '_xhrTransport' : '_iframeTransport';
item.index = item.index || this._nextIndex++;
item.isReady = true;
if (this.isUploading) {
return;
}
this.isUploading = true;
this[ transport ](item);
},
/**
* Cancels uploading of item from the queue
* @param {Item|Number} value
*/
cancelItem: function(value) {
var index = this.getIndexOfItem(value);
var item = this.queue[ index ];
var prop = this.isHTML5 ? '_xhr' : '_form';
item[prop] && item[prop].abort();
},
/**
* Uploads all not uploaded items of queue
*/
uploadAll: function () {
var items = this.getNotUploadedItems().filter(function(item) {
return !item.isUploading;
});
items.forEach(function(item) {
item.index = item.index || this._nextIndex++;
item.isReady = true;
}, this);
items.length && this.uploadItem(items[ 0 ]);
},
/**
* Cancels all uploads
*/
cancelAll: function() {
this.getNotUploadedItems().forEach(function(item) {
this.cancelItem(item);
}, this);
},
/**
* Updates angular scope
* @private
*/
_render: function() {
this.scope.$$phase || this.scope.$digest();
},
/**
* Returns the total progress
* @param {Number} [value]
* @returns {Number}
* @private
*/
_getTotalProgress: function (value) {
if (this.removeAfterUpload) {
return value || 0;
}
var notUploaded = this.getNotUploadedItems().length;
var uploaded = notUploaded ? this.queue.length - notUploaded : this.queue.length;
var ratio = 100 / this.queue.length;
var current = (value || 0) * ratio / 100;
return Math.round(uploaded * ratio + current);
},
/**
* The 'in:progress' handler
* @private
*/
_progress: function (event, item, progress) {
var result = this._getTotalProgress(progress);
this.trigger('progressall', result);
this.progress = result;
this._render();
},
/**
* The 'in:complete' handler
* @private
*/
_complete: function () {
var item = this.getReadyItems()[ 0 ];
this.isUploading = false;
if (angular.isDefined(item)) {
this.uploadItem(item);
return;
}
this.trigger('completeall', this.queue);
this.progress = this._getTotalProgress();
this._render();
},
/**
* The XMLHttpRequest transport
* @private
*/
_xhrTransport: function (item) {
var xhr = item._xhr = new XMLHttpRequest();
var form = new FormData();
var that = this;
this.trigger('beforeupload', item);
item.formData.forEach(function(obj) {
angular.forEach(obj, function(value, key) {
form.append(key, value);
});
});
form.append(item.alias, item.file);
xhr.upload.onprogress = function (event) {
var progress = event.lengthComputable ? event.loaded * 100 / event.total : 0;
that.trigger('in:progress', item, Math.round(progress));
};
xhr.onload = function () {
var response = that._transformResponse(xhr.response);
var event = that._isSuccessCode(xhr.status) ? 'success' : 'error';
that.trigger('in:' + event, xhr, item, response);
that.trigger('in:complete', xhr, item, response);
};
xhr.onerror = function () {
that.trigger('in:error', xhr, item);
that.trigger('in:complete', xhr, item);
};
xhr.onabort = function () {
that.trigger('in:cancel', xhr, item);
that.trigger('in:complete', xhr, item);
};
xhr.open(item.method, item.url, true);
angular.forEach(item.headers, function (value, name) {
xhr.setRequestHeader(name, value);
});
xhr.send(form);
},
/**
* The IFrame transport
* @private
*/
_iframeTransport: function (item) {
var form = angular.element('<form style="display: none;" />');
var iframe = angular.element('<iframe name="iframeTransport' + Date.now() + '">');
var input = item._input;
var that = this;
item._form && item._form.replaceWith(input); // remove old form
item._form = form; // save link to new form
this.trigger('beforeupload', item);
input.prop('name', item.alias);
item.formData.forEach(function(obj) {
angular.forEach(obj, function(value, key) {
form.append(angular.element('<input type="hidden" name="' + key + '" value="' + value + '" />'));
});
});
form.prop({
action: item.url,
method: item.method,
target: iframe.prop('name'),
enctype: 'multipart/form-data',
encoding: 'multipart/form-data' // old IE
});
iframe.bind('load', function () {
// fixed angular.contents() for iframes
var html = iframe[0].contentDocument.body.innerHTML;
var xhr = { response: html, status: 200, dummy: true };
var response = that._transformResponse(xhr.response);
that.trigger('in:success', xhr, item, response);
that.trigger('in:complete', xhr, item, response);
});
form.abort = function() {
var xhr = { status: 0, dummy: true };
iframe.unbind('load').prop('src', 'javascript:false;');
form.replaceWith(input);
that.trigger('in:cancel', xhr, item);
that.trigger('in:complete', xhr, item);
};
input.after(form);
form.append(input).append(iframe);
form[ 0 ].submit();
},
/**
* Checks whether upload successful
* @param {Number} status
* @returns {Boolean}
* @private
*/
_isSuccessCode: function(status) {
return (status >= 200 && status < 300) || status === 304;
},
/**
* Transforms the server response
* @param {*} response
* @returns {*}
* @private
*/
_transformResponse: function (response) {
$http.defaults.transformResponse.forEach(function (transformFn) {
response = transformFn(response);
});
return response;
}
};
/**
* Create a item
* @param {Object} params
* @constructor
*/
function Item(params) {
// fix for old browsers
if (!Uploader.prototype.isHTML5) {
var input = angular.element(params.file);
var clone = $compile(input.clone())(params.uploader.scope);
var value = input.val();
params.file = {
lastModifiedDate: null,
size: null,
type: 'like/' + value.slice(value.lastIndexOf('.') + 1).toLowerCase(),
name: value.slice(value.lastIndexOf('/') + value.lastIndexOf('\\') + 2)
};
params._input = input;
clone.prop('value', null); // FF fix
input.css('display', 'none').after(clone); // remove jquery dependency
}
angular.extend(this, {
isReady: false,
isUploading: false,
isUploaded: false,
isSuccess: false,
isCancel: false,
isError: false,
progress: null,
index: null
}, params);
}
Item.prototype = {
/**
* Link to the constructor
*/
constructor: Item,
/**
* Removes a item
*/
remove: function () {
this.uploader.removeFromQueue(this);
},
/**
* Uploads a item
*/
upload: function () {
this.uploader.uploadItem(this);
},
/**
* Cancels uploading
*/
cancel: function() {
this.uploader.cancelItem(this);
},
/**
* Destroys form and input
* @private
*/
_destroy: function() {
this._form && this._form.remove();
this._input && this._input.remove();
delete this._form;
delete this._input;
},
/**
* The 'beforeupload' handler
* @param {Object} event
* @param {Item} item
* @private
*/
_beforeupload: function (event, item) {
item.isReady = true;
item.isUploading = true;
item.isUploaded = false;
item.isSuccess = false;
item.isCancel = false;
item.isError = false;
item.progress = 0;
},
/**
* The 'in:progress' handler
* @param {Object} event
* @param {Item} item
* @param {Number} progress
* @private
*/
_progress: function (event, item, progress) {
item.progress = progress;
item.uploader.trigger('progress', item, progress);
},
/**
* The 'in:success' handler
* @param {Object} event
* @param {XMLHttpRequest} xhr
* @param {Item} item
* @param {*} response
* @private
*/
_success: function (event, xhr, item, response) {
item.isReady = false;
item.isUploading = false;
item.isUploaded = true;
item.isSuccess = true;
item.isCancel = false;
item.isError = false;
item.progress = 100;
item.index = null;
item.uploader.trigger('success', xhr, item, response);
},
/**
* The 'in:cancel' handler
* @param {Object} event
* @param {XMLHttpRequest} xhr
* @param {Item} item
* @private
*/
_cancel: function(event, xhr, item) {
item.isReady = false;
item.isUploading = false;
item.isUploaded = false;
item.isSuccess = false;
item.isCancel = true;
item.isError = false;
item.progress = 0;
item.index = null;
item.uploader.trigger('cancel', xhr, item);
},
/**
* The 'in:error' handler
* @param {Object} event
* @param {XMLHttpRequest} xhr
* @param {Item} item
* @param {*} response
* @private
*/
_error: function (event, xhr, item, response) {
item.isReady = false;
item.isUploading = false;
item.isUploaded = true;
item.isSuccess = false;
item.isCancel = false;
item.isError = true;
item.progress = 100;
item.index = null;
item.uploader.trigger('error', xhr, item, response);
},
/**
* The 'in:complete' handler
* @param {Object} event
* @param {XMLHttpRequest} xhr
* @param {Item} item
* @param {*} response
* @private
*/
_complete: function (event, xhr, item, response) {
item.uploader.trigger('complete', xhr, item, response);
item.removeAfterUpload && item.remove();
}
};
return {
create: function (params) {
return new Uploader(params);
},
isHTML5: Uploader.prototype.isHTML5
};
}])
return app;
}));

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,97 @@
angular.module('app', ['angularFileUpload'])
// The example of the full functionality
.controller('TestController',function ($scope, $fileUploader) {
'use strict';
// create a uploader with options
var uploader = $scope.uploader = $fileUploader.create({
scope: $scope, // to automatically update the html. Default: $rootScope
url: '/api/containers/container1/upload',
formData: [
{ key: 'value' }
],
filters: [
function (item) { // first user filter
console.info('filter1');
return true;
}
]
});
// ADDING FILTERS
uploader.filters.push(function (item) { // second user filter
console.info('filter2');
return true;
});
// REGISTER HANDLERS
uploader.bind('afteraddingfile', function (event, item) {
console.info('After adding a file', item);
});
uploader.bind('whenaddingfilefailed', function (event, item) {
console.info('When adding a file failed', item);
});
uploader.bind('afteraddingall', function (event, items) {
console.info('After adding all files', items);
});
uploader.bind('beforeupload', function (event, item) {
console.info('Before upload', item);
});
uploader.bind('progress', function (event, item, progress) {
console.info('Progress: ' + progress, item);
});
uploader.bind('success', function (event, xhr, item, response) {
console.info('Success', xhr, item, response);
$scope.$broadcast('uploadCompleted', item);
});
uploader.bind('cancel', function (event, xhr, item) {
console.info('Cancel', xhr, item);
});
uploader.bind('error', function (event, xhr, item, response) {
console.info('Error', xhr, item, response);
});
uploader.bind('complete', function (event, xhr, item, response) {
console.info('Complete', xhr, item, response);
});
uploader.bind('progressall', function (event, progress) {
console.info('Total progress: ' + progress);
});
uploader.bind('completeall', function (event, items) {
console.info('Complete all', items);
});
}
).controller('FilesController', function ($scope, $http) {
$scope.load = function () {
$http.get('/api/containers/container1/files').success(function (data) {
console.log(data);
$scope.files = data;
});
};
$scope.delete = function (index, id) {
$http.delete('/api/containers/container1/files/' + encodeURIComponent(id)).success(function (data, status, headers) {
$scope.files.splice(index, 1);
});
};
$scope.$on('uploadCompleted', function(event) {
console.log('uploadCompleted event received');
$scope.load();
});
});

202
example/public/index.html Normal file
View File

@ -0,0 +1,202 @@
<!DOCTYPE html>
<html id="ng-app" ng-app="app"> <!-- id="ng-app" IE<8 -->
<head>
<title>LoopBack Storage Service Demo</title>
<link rel="stylesheet"
href="//netdna.bootstrapcdn.com/bootstrap/3.0.0/css/bootstrap.min.css"/>
<!-- Fix for old browsers -->
<script src="http://code.jquery.com/jquery-1.8.3.min.js"></script>
<script src="//netdna.bootstrapcdn.com/bootstrap/3.0.0/js/bootstrap.min.js"></script>
<!--<script src="../bower_components/angular/angular.js"></script>-->
<script src="http://code.angularjs.org/1.2.9/angular.min.js"></script>
<script src="angular-file-upload.js"></script>
<script src="controllers.js"></script>
<style>
.my-drop-zone {
border: dotted 3px lightgray;
}
.ng-file-over {
border: dotted 3px red;
}
/* Default class applied to drop zones on over */
.another-file-over-class {
border: dotted 3px green;
}
html, body {
height: 100%;
}
</style>
</head>
<!-- 1. ng-file-drop | ng-file-drop="options" -->
<body ng-controller="TestController" ng-file-drop>
<div class="container">
<div class="navbar navbar-default">
<div class="navbar-header">
<a class="navbar-brand"
href="https://github.com/strongloop/loopback-storage-service">LoopBack
Storage Service</a>
</div>
</div>
<div class="row">
<div class="col-md-3">
<h3>Select files</h3>
<div ng-show="uploader.isHTML5">
<!-- 3. ng-file-over | ng-file-over="className" -->
<div class="well my-drop-zone" ng-file-over>
Base drop zone
</div>
<!-- Example: ng-file-drop | ng-file-drop="options" -->
<div class="well my-drop-zone" ng-file-drop="{ url: '/foo' }"
ng-file-over="another-file-over-class">
Another drop zone with its own settings
</div>
</div>
<!-- 2. ng-file-select | ng-file-select="options" -->
Multiple
<input ng-file-select type="file" multiple/><br/>
Single
<input ng-file-select type="file"/>
</div>
<div class="col-md-9" style="margin-bottom: 40px">
<h3>Upload queue</h3>
<p>Queue length: {{ uploader.queue.length }}</p>
<table class="table">
<thead>
<tr>
<th width="50%">Name</th>
<th ng-show="uploader.isHTML5">Size</th>
<th ng-show="uploader.isHTML5">Progress</th>
<th>Status</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<tr ng-repeat="item in uploader.queue">
<td><strong>{{ item.file.name }}</strong></td>
<td ng-show="uploader.isHTML5" nowrap>{{
item.file.size/1024/1024|number:2 }} MB
</td>
<td ng-show="uploader.isHTML5">
<div class="progress" style="margin-bottom: 0;">
<div class="progress-bar" role="progressbar"
ng-style="{ 'width': item.progress + '%' }"></div>
</div>
</td>
<td class="text-center">
<span ng-show="item.isSuccess"><i
class="glyphicon glyphicon-ok"></i></span>
<span ng-show="item.isCancel"><i
class="glyphicon glyphicon-ban-circle"></i></span>
<span ng-show="item.isError"><i
class="glyphicon glyphicon-remove"></i></span>
</td>
<td nowrap>
<button type="button" class="btn btn-success btn-xs"
ng-click="item.upload()"
ng-disabled="item.isReady || item.isUploading || item.isSuccess">
<span class="glyphicon glyphicon-upload"></span>
Upload
</button>
<button type="button" class="btn btn-warning btn-xs"
ng-click="item.cancel()"
ng-disabled="!item.isUploading">
<span class="glyphicon glyphicon-ban-circle"></span>
Cancel
</button>
<button type="button" class="btn btn-danger btn-xs"
ng-click="item.remove()">
<span class="glyphicon glyphicon-trash"></span>
Remove
</button>
</td>
</tr>
</tbody>
</table>
<div>
<p>
Queue progress:
<div class="progress" style="">
<div class="progress-bar" role="progressbar"
ng-style="{ 'width': uploader.progress + '%' }"></div>
</div>
</p>
<button type="button" class="btn btn-success btn-s"
ng-click="uploader.uploadAll()"
ng-disabled="!uploader.getNotUploadedItems().length">
<span class="glyphicon glyphicon-upload"></span> Upload all
</button>
<button type="button" class="btn btn-warning btn-s"
ng-click="uploader.cancelAll()"
ng-disabled="!uploader.isUploading">
<span class="glyphicon glyphicon-ban-circle"></span> Cancel
all
</button>
<button type="button" class="btn btn-danger btn-s"
ng-click="uploader.clearQueue()"
ng-disabled="!uploader.queue.length">
<span class="glyphicon glyphicon-trash"></span> Remove all
</button>
</div>
</div>
<div class="col-md-9" style="margin-bottom: 40px"
ng-controller="FilesController" data-ng-init="load()">
<h3>Files in the container</h3>
<table class="table">
<tbody>
<tr ng-repeat="file in files">
<td>
<a href="/api/containers/container1/download/{{file.name}}"><strong>{{
file.name }}</strong></a></td>
<td>
<td>
<button type="button" class="btn btn-danger btn-xs"
ng-click="delete($index, file.name)"
title="Delete the file">
<span class="glyphicon glyphicon-trash"></span>
Remove
</button>
</td>
</button>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</body>
</html>

View File

@ -1,78 +1,78 @@
var StorageService = require('../');
var StorageService = require('../').StorageService;
var providers = require('./providers.json');
var express = require('express');
var app = express();
app.configure(function () {
app.set('port', process.env.PORT || 3001);
app.set('views', __dirname + '/views');
app.set('view engine', 'ejs');
app.use(express.favicon());
// app.use(express.logger('dev'));
app.use(express.methodOverride());
app.use(app.router);
app.set('port', process.env.PORT || 3001);
app.set('views', __dirname + '/views');
app.set('view engine', 'ejs');
app.use(express.favicon());
// app.use(express.logger('dev'));
app.use(express.methodOverride());
app.use(app.router);
});
var handler = new StorageService(
{
{
provider: 'amazon',
key: 'your-amazon-key',
keyId: 'your-amazon-key-id'
});
key: providers.amazon.key,
keyId: providers.amazon.keyId
});
app.get('/', function (req, res, next) {
res.setHeader('Content-Type', 'text/html');
var form = "<html><body><h1>Storage Service Demo</h1>" +
"<a href='/download'>List all containers</a><p>" +
"Upload to container con1: <p>" +
"<form method='POST' enctype='multipart/form-data' action='/upload/con1'>"
+ "File to upload: <input type=file name=uploadedFiles multiple=true><br>"
+ "Notes about the file: <input type=text name=note><br>"
+ "<input type=submit value=Upload></form>" +
"</body></html>";
res.send(form);
res.end();
res.setHeader('Content-Type', 'text/html');
var form = "<html><body><h1>Storage Service Demo</h1>" +
"<a href='/download'>List all containers</a><p>" +
"Upload to container con1: <p>" +
"<form method='POST' enctype='multipart/form-data' action='/upload/con1'>"
+ "File to upload: <input type=file name=uploadedFiles multiple=true><br>"
+ "Notes about the file: <input type=text name=note><br>"
+ "<input type=submit value=Upload></form>" +
"</body></html>";
res.send(form);
res.end();
});
app.post('/upload/:container', function (req, res, next) {
handler.upload(req, res, function (err, result) {
if (!err) {
res.setHeader('Content-Type', 'application/json');
res.send(200, result);
} else {
res.send(500, err);
}
});
handler.upload(req, res, function (err, result) {
if (!err) {
res.setHeader('Content-Type', 'application/json');
res.send(200, result);
} else {
res.send(500, err);
}
});
});
app.get('/download', function (req, res, next) {
handler.getContainers(function (err, containers) {
var html = "<html><body><h1>Containers</h1><ul>";
containers.forEach(function (f) {
html += "<li><a href='/download/" + f.name + "'>" + f.name + "</a></li>"
});
html += "</ul><p><a href='/'>Home</a></p></body></html>";
res.send(200, html);
handler.getContainers(function (err, containers) {
var html = "<html><body><h1>Containers</h1><ul>";
containers.forEach(function (f) {
html += "<li><a href='/download/" + f.name + "'>" + f.name + "</a></li>"
});
html += "</ul><p><a href='/'>Home</a></p></body></html>";
res.send(200, html);
});
});
app.get('/download/:container', function (req, res, next) {
handler.getFiles(req.params.container, function (err, files) {
var html = "<html><body><h1>Files in container " + req.params.container + "</h1><ul>";
files.forEach(function (f) {
html += "<li><a href='/download/" + f.container + "/" + f.name + "'>" + f.container + "/" + f.name + "</a></li>"
});
html += "</ul><p><a href='/'>Home</a></p></body></html>";
res.send(200, html);
handler.getFiles(req.params.container, function (err, files) {
var html = "<html><body><h1>Files in container " + req.params.container + "</h1><ul>";
files.forEach(function (f) {
html += "<li><a href='/download/" + f.container + "/" + f.name + "'>" + f.container + "/" + f.name + "</a></li>"
});
html += "</ul><p><a href='/'>Home</a></p></body></html>";
res.send(200, html);
});
});
app.get('/download/:container/:file', function (req, res, next) {
handler.download(req, res, function (err, result) {
if (err) {
res.send(500, err);
}
});
handler.download(req.params.container, req.params.file, res, function (err, result) {
if (err) {
res.send(500, err);
}
});
});
app.listen(app.get('port'));

View File

@ -1,16 +1,16 @@
var StorageService = require('../');
var StorageService = require('../').StorageService;
var express = require('express');
var app = express();
app.configure(function () {
app.set('port', process.env.PORT || 3001);
app.set('views', __dirname + '/views');
app.set('view engine', 'ejs');
app.use(express.favicon());
// app.use(express.logger('dev'));
app.use(express.methodOverride());
app.use(app.router);
app.set('port', process.env.PORT || 3001);
app.set('views', __dirname + '/views');
app.set('view engine', 'ejs');
app.use(express.favicon());
// app.use(express.logger('dev'));
app.use(express.methodOverride());
app.use(app.router);
});
// Create the container
@ -20,58 +20,58 @@ mkdirp.sync('/tmp/storage/con1');
var handler = new StorageService({provider: 'filesystem', root: '/tmp/storage'});
app.get('/', function (req, res, next) {
res.setHeader('Content-Type', 'text/html');
var form = "<html><body><h1>Storage Service Demo</h1>" +
"<a href='/download'>List all containers</a><p>" +
"Upload to container con1: <p>" +
"<form method='POST' enctype='multipart/form-data' action='/upload/con1'>"
+ "File to upload: <input type=file name=uploadedFiles multiple=true><br>"
+ "Notes about the file: <input type=text name=note><br>"
+ "<input type=submit value=Upload></form>" +
"</body></html>";
res.send(form);
res.end();
res.setHeader('Content-Type', 'text/html');
var form = "<html><body><h1>Storage Service Demo</h1>" +
"<a href='/download'>List all containers</a><p>" +
"Upload to container con1: <p>" +
"<form method='POST' enctype='multipart/form-data' action='/upload/con1'>"
+ "File to upload: <input type=file name=uploadedFiles multiple=true><br>"
+ "Notes about the file: <input type=text name=note><br>"
+ "<input type=submit value=Upload></form>" +
"</body></html>";
res.send(form);
res.end();
});
app.post('/upload/:container', function (req, res, next) {
handler.upload(req, res, function (err, result) {
if (!err) {
res.setHeader('Content-Type', 'application/json');
res.send(200, result);
} else {
res.send(500, err);
}
});
handler.upload(req, res, function (err, result) {
if (!err) {
res.setHeader('Content-Type', 'application/json');
res.send(200, result);
} else {
res.send(500, err);
}
});
});
app.get('/download', function (req, res, next) {
handler.getContainers(function (err, containers) {
var html = "<html><body><h1>Containers</h1><ul>";
containers.forEach(function (f) {
html += "<li><a href='/download/" + f.name + "'>" + f.name + "</a></li>"
});
html += "</ul><p><a href='/'>Home</a></p></body></html>";
res.send(200, html);
handler.getContainers(function (err, containers) {
var html = "<html><body><h1>Containers</h1><ul>";
containers.forEach(function (f) {
html += "<li><a href='/download/" + f.name + "'>" + f.name + "</a></li>"
});
html += "</ul><p><a href='/'>Home</a></p></body></html>";
res.send(200, html);
});
});
app.get('/download/:container', function (req, res, next) {
handler.getFiles(req.params.container, function (err, files) {
var html = "<html><body><h1>Files in container " + req.params.container + "</h1><ul>";
files.forEach(function (f) {
html += "<li><a href='/download/" + f.container + "/" + f.name + "'>" + f.container + "/" + f.name + "</a></li>"
});
html += "</ul><p><a href='/'>Home</a></p></body></html>";
res.send(200, html);
handler.getFiles(req.params.container, function (err, files) {
var html = "<html><body><h1>Files in container " + req.params.container + "</h1><ul>";
files.forEach(function (f) {
html += "<li><a href='/download/" + f.container + "/" + f.name + "'>" + f.container + "/" + f.name + "</a></li>"
});
html += "</ul><p><a href='/'>Home</a></p></body></html>";
res.send(200, html);
});
});
app.get('/download/:container/:file', function (req, res, next) {
handler.download(req, res, function (err, result) {
if (err) {
res.send(500, err);
}
});
handler.download(req.params.container, req.params.file, res, function (err, result) {
if (err) {
res.send(500, err);
}
});
});
app.listen(app.get('port'));

5
index.js Normal file
View File

@ -0,0 +1,5 @@
var StorageConnector = require('./lib/storage-connector');
StorageConnector.StorageService = require('./lib/storage-service');
module.exports = StorageConnector;

View File

@ -1,22 +1,84 @@
var pkgcloud = require('pkgcloud');
/*!
* Patch the prototype for a given subclass of Container or File
* @param {Function} cls The subclass
*/
function patchBaseClass(cls) {
var proto = cls.prototype;
var found = false;
// Find the prototype that owns the _setProperties method
while (proto
&& proto.constructor !== pkgcloud.storage.Container
&& proto.constructor !== pkgcloud.storage.File) {
if (proto.hasOwnProperty('_setProperties')) {
found = true;
break;
} else {
proto = Object.getPrototypeOf(proto);
}
}
if (!found) {
proto = cls.prototype;
}
var m1 = proto._setProperties;
proto._setProperties = function (details) {
// Use an empty object to receive the calculated properties from details
var receiver = {};
m1.call(receiver, details);
// Apply the calculated properties to this
for (var p in receiver) {
this[p] = receiver[p];
}
// Keep references to raw and the calculated properties
this._rawMetadata = details;
this._metadata = receiver; // Use _metadata to avoid conflicts
}
proto.toJSON = function () {
return this._metadata;
};
proto.getMetadata = function () {
return this._metadata;
};
proto.getRawMetadata = function () {
return this._rawMetadata;
};
}
/*!
* Patch the pkgcloud Container/File classes so that the metadata are separately
* stored for JSON serialization
*
* @param {String} provider The name of the storage provider
*/
function patchContainerAndFileClass(provider) {
var storageProvider = getProvider(provider).storage;
patchBaseClass(storageProvider.Container);
patchBaseClass(storageProvider.File);
}
/**
* Create a client instance based on the options
* @param options
* @returns {*}
*/
function createClient(options) {
options = options || {};
var provider = options.provider || 'filesystem';
var handler;
options = options || {};
var provider = options.provider || 'filesystem';
var handler;
try {
// Try to load the provider from providers folder
handler = require('./providers/' + provider);
} catch (err) {
// Fall back to pkgcloud
handler = require('pkgcloud').storage;
}
return handler.createClient(options);
try {
// Try to load the provider from providers folder
handler = require('./providers/' + provider);
} catch (err) {
// Fall back to pkgcloud
handler = require('pkgcloud').storage;
}
patchContainerAndFileClass(provider);
return handler.createClient(options);
}
/**
@ -25,13 +87,13 @@ function createClient(options) {
* @returns {*}
*/
function getProvider(provider) {
try {
// Try to load the provider from providers folder
return require('./providers/' + provider);
} catch (err) {
// Fall back to pkgcloud
return require('pkgcloud').providers[provider];
}
try {
// Try to load the provider from providers folder
return require('./providers/' + provider);
} catch (err) {
// Fall back to pkgcloud
return require('pkgcloud').providers[provider];
}
}
module.exports.createClient = createClient;

View File

@ -1,150 +0,0 @@
var factory = require('./factory');
var handler = require('./storage-handler');
var storage = require('pkgcloud').storage;
module.exports = StorageService;
/**
* @param options The options to create a provider
* @returns {StorageService}
* @constructor
*/
function StorageService(options) {
if (!(this instanceof StorageService)) {
return new StorageService(options);
}
this.provider = options.provider;
this.client = factory.createClient(options);
}
StorageService.prototype.getContainers = function (cb) {
return this.client.getContainers(cb);
}
StorageService.prototype.createContainer = function (options, cb) {
options = options || {};
if('object' === typeof options && !(options instanceof storage.Container)) {
var Container = factory.getProvider(this.provider).Container;
options = new Container(this.client, options);
}
return this.client.createContainer(options, cb);
}
StorageService.prototype.destroyContainer = function (container, cb) {
return this.client.destroyContainer(container, cb);
}
StorageService.prototype.getContainer = function (container, cb) {
return this.client.getContainer(container, cb);
}
// File related functions
StorageService.prototype.uploadStream = function (container, file, options, cb) {
if(!cb && typeof options === 'function') {
cb = options;
options = {};
}
options = options || {};
if(container) options.container = container;
if(file) options.remote = file;
return this.client.upload(options, cb);
}
StorageService.prototype.downloadStream = function (container, file, options, cb) {
if(!cb && typeof options === 'function') {
cb = options;
options = {};
}
options = options || {};
if(container) options.container = container;
if(file) options.remote = file;
return this.client.download(options, cb);
}
StorageService.prototype.getFiles = function (container, download, cb) {
return this.client.getFiles(container, download, cb);
}
StorageService.prototype.getFile = function (container, file, cb) {
return this.client.getFile(container, file, cb);
}
StorageService.prototype.removeFile = function (container, file, cb) {
return this.client.removeFile(container, file, cb);
}
StorageService.prototype.upload = function (req, res, cb) {
return handler.upload(this.client, req, res, cb);
}
StorageService.prototype.download = function (req, res, cb) {
return handler.download(this.client, req, res, cb);
}
StorageService.modelName = 'storage';
StorageService.prototype.getContainers.shared = true;
StorageService.prototype.getContainers.accepts = [];
StorageService.prototype.getContainers.returns = {arg: 'containers', type: 'array'};
StorageService.prototype.getContainers.http = [
{verb: 'get', path: '/'}
];
StorageService.prototype.getContainer.shared = true;
StorageService.prototype.getContainer.accepts = [{arg: 'container', type: 'string'}];
StorageService.prototype.getContainer.returns = {arg: 'container', type: 'object'};
StorageService.prototype.getContainer.http = [
{verb: 'get', path: '/:container'}
];
StorageService.prototype.createContainer.shared = true;
StorageService.prototype.createContainer.accepts = [{arg: 'options', type: 'object'}];
StorageService.prototype.createContainer.returns = {arg: 'container', type: 'object'};
StorageService.prototype.createContainer.http = [
{verb: 'post', path: '/'}
];
StorageService.prototype.destroyContainer.shared = true;
StorageService.prototype.destroyContainer.accepts = [{arg: 'container', type: 'string'}];
StorageService.prototype.destroyContainer.returns = {};
StorageService.prototype.destroyContainer.http = [
{verb: 'delete', path: '/:container'}
];
StorageService.prototype.getFiles.shared = true;
StorageService.prototype.getFiles.accepts = [{arg: 'container', type: 'string'}];
StorageService.prototype.getFiles.returns = {arg: 'files', type: 'array'};
StorageService.prototype.getFiles.http = [
{verb: 'get', path: '/:container/files'}
];
StorageService.prototype.getFile.shared = true;
StorageService.prototype.getFile.accepts = [{arg: 'container', type: 'string'}, {arg: 'file', type: 'string'}];
StorageService.prototype.getFile.returns = {arg: 'file', type: 'object'};
StorageService.prototype.getFile.http = [
{verb: 'get', path: '/:container/files/:file'}
];
StorageService.prototype.removeFile.shared = true;
StorageService.prototype.removeFile.accepts = [{arg: 'container', type: 'string'}, {arg: 'file', type: 'string'}];
StorageService.prototype.removeFile.returns = {};
StorageService.prototype.removeFile.http = [
{verb: 'delete', path: '/:container/files/:file'}
];
StorageService.prototype.upload.shared = true;
StorageService.prototype.upload.accepts = [{arg: 'req', type: 'undefined', 'http': {source: 'req'}}];
StorageService.prototype.upload.returns = {arg: 'result', type: 'object'};
StorageService.prototype.upload.http = [
{verb: 'post', path: '/:container/upload/:file'}
];
StorageService.prototype.download.shared = true;
StorageService.prototype.download.accepts = [{arg: 'req', type: 'undefined', 'http': {source: 'req'}}];
StorageService.prototype.download.returns = {arg: 'res', type: 'stream'};
StorageService.prototype.download.http = [
{verb: 'get', path: '/:container/download/:file'}
];

View File

@ -4,15 +4,15 @@ var util = require('util');
exports.Container = Container;
function Container(client, details) {
base.Container.call(this, client, details);
};
base.Container.call(this, client, details);
}
util.inherits(Container, base.Container);
Container.prototype._setProperties = function(details) {
for(var k in details) {
if(typeof details[k] !== 'function') {
this[k] = details[k];
}
Container.prototype._setProperties = function (details) {
for (var k in details) {
if (typeof details[k] !== 'function') {
this[k] = details[k];
}
}
}
};

View File

@ -4,15 +4,15 @@ var util = require('util');
exports.File = File;
function File(client, details) {
base.File.call(this, client, details);
};
base.File.call(this, client, details);
}
util.inherits(File, base.File);
File.prototype._setProperties = function(details) {
for(var k in details) {
if(typeof details[k] !== 'function') {
this[k] = details[k];
}
File.prototype._setProperties = function (details) {
for (var k in details) {
if (typeof details[k] !== 'function') {
this[k] = details[k];
}
}
}
};

View File

@ -3,215 +3,251 @@
*/
var fs = require('fs'),
path = require('path'),
async = require('async'),
File = require('./file').File,
Container = require('./container').Container;
path = require('path'),
async = require('async'),
File = require('./file').File,
Container = require('./container').Container;
module.exports.storage = module.exports; // To make it consistent with pkgcloud
module.exports.File = File;
module.exports.Container = Container;
module.exports.Client = FileSystemProvider;
module.exports.createClient = function (options) {
return new FileSystemProvider(options);
return new FileSystemProvider(options);
};
function FileSystemProvider(options) {
options = options || {};
this.root = options.root;
var exists = fs.existsSync(this.root);
if (!exists) {
throw new Error('Path does not exist: ' + this.root);
}
var stat = fs.statSync(this.root);
if (!stat.isDirectory()) {
throw new Error('Invalid directory: ' + this.root);
}
options = options || {};
this.root = options.root;
var exists = fs.existsSync(this.root);
if (!exists) {
throw new Error('FileSystemProvider: Path does not exist: ' + this.root);
}
var stat = fs.statSync(this.root);
if (!stat.isDirectory()) {
throw new Error('FileSystemProvider: Invalid directory: ' + this.root);
}
}
var namePattern = new RegExp('[^' + path.sep + '/]+');
function validateName(name, cb) {
if (!name) {
cb && process.nextTick(cb.bind(null, new Error('Invalid name: ' + name)));
if(!cb) {
console.error('Invalid name: ', name);
}
return false;
if (!name) {
cb && process.nextTick(cb.bind(null, new Error('Invalid name: ' + name)));
if (!cb) {
console.error('FileSystemProvider: Invalid name: ', name);
}
var match = namePattern.exec(name);
if (match && match.index === 0 && match[0].length === name.length) {
return true;
} else {
cb && process.nextTick(cb.bind(null, new Error('Invalid name: ' + name)));
if(!cb) {
console.error('Invalid name: ', name);
}
return false;
return false;
}
var match = namePattern.exec(name);
if (match && match.index === 0 && match[0].length === name.length) {
return true;
} else {
cb && process.nextTick(cb.bind(null,
new Error('FileSystemProvider: Invalid name: ' + name)));
if (!cb) {
console.error('FileSystemProvider: Invalid name: ', name);
}
return false;
}
}
// Container related functions
FileSystemProvider.prototype.getContainers = function (cb) {
var self = this;
fs.readdir(self.root, function (err, files) {
var containers = [];
var tasks = [];
files.forEach(function (f) {
tasks.push(fs.stat.bind(null, path.join(self.root, f)));
});
async.parallel(tasks, function (err, stats) {
if (err) {
cb && cb(err);
} else {
stats.forEach(function (stat, index) {
if (stat.isDirectory()) {
var name = files[index];
var props = {name: name};
for (var p in stat) {
props[p] = stat[p];
}
var container = new Container(self, props);
containers.push(container);
}
});
cb && cb(err, containers);
}
});
});
/*!
* Populate the metadata from file stat into props
* @param {fs.Stats} stat The file stat instance
* @param {Object} props The metadata object
*/
function populateMetadata(stat, props) {
for (var p in stat) {
switch (p) {
case 'size':
case 'atime':
case 'mtime':
case 'ctime':
props[p] = stat[p];
break;
}
}
}
FileSystemProvider.prototype.getContainers = function (cb) {
var self = this;
fs.readdir(self.root, function (err, files) {
var containers = [];
var tasks = [];
files.forEach(function (f) {
tasks.push(fs.stat.bind(fs, path.join(self.root, f)));
});
async.parallel(tasks, function (err, stats) {
if (err) {
cb && cb(err);
} else {
stats.forEach(function (stat, index) {
if (stat.isDirectory()) {
var name = files[index];
var props = {name: name};
populateMetadata(stat, props);
var container = new Container(self, props);
containers.push(container);
}
});
cb && cb(err, containers);
}
});
});
};
FileSystemProvider.prototype.createContainer = function (options, cb) {
var self = this;
var name = options.name;
validateName(name, cb) && fs.mkdir(path.join(this.root, name), options, function (err) {
cb && cb(err, new Container(self, {name: name}));
var self = this;
var name = options.name;
var dir = path.join(this.root, name);
validateName(name, cb) && fs.mkdir(dir, options, function (err) {
if(err) {
return cb && cb(err);
}
fs.stat(dir, function (err, stat) {
var container = null;
if (!err) {
var props = {name: name};
populateMetadata(stat, props);
container = new Container(self, props);
}
cb && cb(err, container);
});
}
});
};
FileSystemProvider.prototype.destroyContainer = function (containerName, cb) {
if (!validateName(containerName, cb)) return;
if (!validateName(containerName, cb)) return;
var dir = path.join(this.root, containerName);
fs.readdir(dir, function (err, files) {
var tasks = [];
files.forEach(function (f) {
tasks.push(fs.unlink.bind(null, path.join(dir, f)));
});
async.parallel(tasks, function (err) {
if (err) {
cb && cb(err);
} else {
fs.rmdir(dir, cb);
}
});
var dir = path.join(this.root, containerName);
fs.readdir(dir, function (err, files) {
var tasks = [];
files.forEach(function (f) {
tasks.push(fs.unlink.bind(fs, path.join(dir, f)));
});
}
async.parallel(tasks, function (err) {
if (err) {
cb && cb(err);
} else {
fs.rmdir(dir, cb);
}
});
});
};
FileSystemProvider.prototype.getContainer = function (containerName, cb) {
var self = this;
if (!validateName(containerName, cb)) return;
var dir = path.join(this.root, containerName);
fs.stat(dir, function (err, stat) {
var container = null;
if (!err) {
var props = {name: containerName};
for (var p in stat) {
props[p] = stat[p];
}
container = new Container(self, props);
}
cb && cb(err, container);
});
}
var self = this;
if (!validateName(containerName, cb)) return;
var dir = path.join(this.root, containerName);
fs.stat(dir, function (err, stat) {
var container = null;
if (!err) {
var props = {name: containerName};
populateMetadata(stat, props);
container = new Container(self, props);
}
cb && cb(err, container);
});
};
// File related functions
FileSystemProvider.prototype.upload = function (options, cb) {
var container = options.container;
if (!validateName(container, cb)) return;
var file = options.remote;
if (!validateName(file, cb)) return;
var filePath = path.join(this.root, container, file);
var container = options.container;
if (!validateName(container, cb)) return;
var file = options.remote;
if (!validateName(file, cb)) return;
var filePath = path.join(this.root, container, file);
var fileOpts = {flags: 'w+',
encoding: null,
mode: 0666 };
var fileOpts = {flags: options.flags || 'w+',
encoding: options.encoding || null,
mode: options.mode || 0666
};
try {
return fs.createWriteStream(filePath, fileOpts);
}
} catch (e) {
cb && cb(e);
}
};
FileSystemProvider.prototype.download = function (options, cb) {
var container = options.container;
if (!validateName(container, cb)) return;
var file = options.remote;
if (!validateName(file, cb)) return;
var container = options.container;
if (!validateName(container, cb)) return;
var file = options.remote;
if (!validateName(file, cb)) return;
var filePath = path.join(this.root, container, file);
var filePath = path.join(this.root, container, file);
var fileOpts = {flags: 'r',
autoClose: true };
var fileOpts = {flags: 'r',
autoClose: true };
try {
return fs.createReadStream(filePath, fileOpts);
}
} catch (e) {
cb && cb(e);
}
};
FileSystemProvider.prototype.getFiles = function (container, download, cb) {
if (typeof download === 'function' && !(download instanceof RegExp)) {
cb = download;
download = false;
}
var self = this;
if (!validateName(container, cb)) return;
var dir = path.join(this.root, container);
fs.readdir(dir, function (err, entries) {
var files = [];
var tasks = [];
entries.forEach(function (f) {
tasks.push(fs.stat.bind(null, path.join(dir, f)));
});
async.parallel(tasks, function (err, stats) {
if (err) {
cb && cb(err);
} else {
stats.forEach(function (stat, index) {
if (stat.isFile()) {
var props = {container: container, name: entries[index]};
for (var p in stat) {
props[p] = stat[p];
}
var file = new File(self, props);
files.push(file);
}
});
cb && cb(err, files);
}
});
if (typeof download === 'function' && !(download instanceof RegExp)) {
cb = download;
download = false;
}
var self = this;
if (!validateName(container, cb)) return;
var dir = path.join(this.root, container);
fs.readdir(dir, function (err, entries) {
var files = [];
var tasks = [];
entries.forEach(function (f) {
tasks.push(fs.stat.bind(fs, path.join(dir, f)));
});
}
async.parallel(tasks, function (err, stats) {
if (err) {
cb && cb(err);
} else {
stats.forEach(function (stat, index) {
if (stat.isFile()) {
var props = {container: container, name: entries[index]};
populateMetadata(stat, props);
var file = new File(self, props);
files.push(file);
}
});
cb && cb(err, files);
}
});
});
};
FileSystemProvider.prototype.getFile = function (container, file, cb) {
var self = this;
if (!validateName(container, cb)) return;
if (!validateName(file, cb)) return;
var filePath = path.join(this.root, container, file);
fs.stat(filePath, function (err, stat) {
var f = null;
if (!err) {
var props = {container: container, name: file};
for (var p in stat) {
props[p] = stat[p];
}
f = new File(self, props);
}
cb && cb(err, f);
});
}
var self = this;
if (!validateName(container, cb)) return;
if (!validateName(file, cb)) return;
var filePath = path.join(this.root, container, file);
fs.stat(filePath, function (err, stat) {
var f = null;
if (!err) {
var props = {container: container, name: file};
populateMetadata(stat, props);
f = new File(self, props);
}
cb && cb(err, f);
});
};
FileSystemProvider.prototype.getUrl = function (options) {
options = options || {};
var filePath = path.join(this.root, options.container, options.path);
return filePath;
};
FileSystemProvider.prototype.removeFile = function (container, file, cb) {
if (!validateName(container, cb)) return;
if (!validateName(file, cb)) return;
if (!validateName(container, cb)) return;
if (!validateName(file, cb)) return;
var filePath = path.join(this.root, container, file);
fs.unlink(filePath, cb);
}
var filePath = path.join(this.root, container, file);
fs.unlink(filePath, cb);
};

View File

@ -1,26 +1,28 @@
var StorageService = require('./index');
var StorageService = require('./storage-service');
/**
* Export the initialize method to Loopback data
* @param dataSource
* @param callback
*/
exports.initialize = function (dataSource, callback) {
var settings = dataSource.settings || {};
var settings = dataSource.settings || {};
var connector = new StorageService(settings);
dataSource.connector = connector;
dataSource.connector.dataSource = dataSource;
var connector = new StorageService(settings);
dataSource.connector = connector;
dataSource.connector.dataSource = dataSource;
connector.DataAccessObject = function() {};
for (var m in StorageService.prototype) {
var method = StorageService.prototype[m];
if ('function' === typeof method) {
connector.DataAccessObject[m] = method.bind(connector);
for(var k in method) {
connector.DataAccessObject[m][k] = method[k];
}
}
connector.DataAccessObject = function () {
};
for (var m in StorageService.prototype) {
var method = StorageService.prototype[m];
if ('function' === typeof method) {
connector.DataAccessObject[m] = method.bind(connector);
for (var k in method) {
connector.DataAccessObject[m][k] = method[k];
}
}
}
connector.define = function(model, properties, settings) {};
}
connector.define = function (model, properties, settings) {
};
};

View File

@ -4,122 +4,124 @@ var StringDecoder = require('string_decoder').StringDecoder;
/**
* Handle multipart/form-data upload to the storage service
* @param provider The storage service provider
* @param req The HTTP request
* @param res The HTTP response
* @param cb The callback
* @param {Request} req The HTTP request
* @param {Response} res The HTTP response
* @param {String} container The container name
* @param {Function} cb The callback
*/
exports.upload = function (provider, req, res, cb) {
var form = new IncomingForm(this.options);
var container = req.params.container;
var fields = {}, files = {};
form.handlePart = function (part) {
var self = this;
exports.upload = function (provider, req, res, container, cb) {
var form = new IncomingForm(this.options);
container = container || req.params.container;
var fields = {}, files = {};
form.handlePart = function (part) {
var self = this;
if (part.filename === undefined) {
var value = ''
, decoder = new StringDecoder(this.encoding);
if (part.filename === undefined) {
var value = ''
, decoder = new StringDecoder(this.encoding);
part.on('data', function (buffer) {
self._fieldsSize += buffer.length;
if (self._fieldsSize > self.maxFieldsSize) {
self._error(new Error('maxFieldsSize exceeded, received ' + self._fieldsSize + ' bytes of field data'));
return;
}
value += decoder.write(buffer);
});
part.on('end', function () {
var values = fields[part.name];
if(values === undefined) {
values = [value];
fields[part.name] = values;
} else {
values.push(value);
}
self.emit('field', part.name, value);
});
return;
part.on('data', function (buffer) {
self._fieldsSize += buffer.length;
if (self._fieldsSize > self.maxFieldsSize) {
self._error(new Error('maxFieldsSize exceeded, received ' + self._fieldsSize + ' bytes of field data'));
return;
}
value += decoder.write(buffer);
});
this._flushing++;
var file = {
container: container,
name: part.filename,
type: part.mime
};
self.emit('fileBegin', part.name, file);
var headers = {};
if('content-type' in part.headers) {
headers['content-type'] = part.headers['content-type'];
part.on('end', function () {
var values = fields[part.name];
if (values === undefined) {
values = [value];
fields[part.name] = values;
} else {
values.push(value);
}
var writer = provider.upload({container: container, remote: part.filename});
self.emit('field', part.name, value);
});
return;
}
var endFunc = function () {
self._flushing--;
var values = files[part.name];
if(values === undefined) {
values = [file];
files[part.name] = values;
} else {
values.push(file);
}
self.emit('file', part.name, file);
self._maybeEnd();
};
this._flushing++;
/*
part.on('data', function (buffer) {
self.pause();
writer.write(buffer, function () {
// pkgcloud stream doesn't make callbacks
});
self.resume();
});
part.on('end', function () {
writer.end(); // pkgcloud stream doesn't make callbacks
endFunc();
});
*/
part.pipe(writer, { end: false });
part.on("end", function() {
writer.end();
endFunc();
});
var file = {
container: container,
name: part.filename,
type: part.mime
};
form.parse(req, function (err, _fields, _files) {
if(err) {
console.error(err);
}
cb && cb(err, {files: files, fields: fields});
self.emit('fileBegin', part.name, file);
var headers = {};
if ('content-type' in part.headers) {
headers['content-type'] = part.headers['content-type'];
}
var writer = provider.upload({container: container, remote: part.filename});
var endFunc = function () {
self._flushing--;
var values = files[part.name];
if (values === undefined) {
values = [file];
files[part.name] = values;
} else {
values.push(file);
}
self.emit('file', part.name, file);
self._maybeEnd();
};
/*
part.on('data', function (buffer) {
self.pause();
writer.write(buffer, function () {
// pkgcloud stream doesn't make callbacks
});
self.resume();
});
part.on('end', function () {
writer.end(); // pkgcloud stream doesn't make callbacks
endFunc();
});
*/
part.pipe(writer, { end: false });
part.on("end", function () {
writer.end();
endFunc();
});
};
form.parse(req, function (err, _fields, _files) {
if (err) {
console.error(err);
}
cb && cb(err, {files: files, fields: fields});
});
}
/**
* Handle download from a container/file
* @param provider The storage service provider
* @param req The HTTP request
* @param res The HTTP response
* @param cb The callback
* @param {Request} req The HTTP request
* @param {Response} res The HTTP response
* @param {String} container The container name
* @param {String} file The file name
* @param {Function} cb The callback
*/
exports.download = function(provider, req, res, cb) {
var reader = provider.download({
container: req.params.container,
remote: req.params.file
});
reader.pipe(res);
reader.on('error', function(err) {
cb && cb(err);
});
reader.on('end', function(err, result) {
cb && cb(err, result);
});
exports.download = function (provider, req, res, container, file, cb) {
var reader = provider.download({
container: container || req && req.params.container,
remote: file || req && req.params.file
});
res.type(file);
reader.pipe(res);
reader.on('error', function (err) {
res.type('application/json');
res.send(500, { error: err });
});
}

262
lib/storage-service.js Normal file
View File

@ -0,0 +1,262 @@
var factory = require('./factory');
var handler = require('./storage-handler');
var storage = require('pkgcloud').storage;
module.exports = StorageService;
/**
* Storage service constructor. Properties of options object depend on the storage service provider.
*
*
* @options {Object} options The options to create a provider; see below;
* @prop {Object} connector <!-- What is this? -->
* @prop {String} provider Use 'filesystem' for local file system. Other supported values are: 'amazon', 'rackspace', 'azure', and 'openstack'.
* @prop {String} root With 'filesystem' provider, the path to the root of storage directory.
* @class
*/
function StorageService(options) {
if (!(this instanceof StorageService)) {
return new StorageService(options);
}
this.provider = options.provider;
this.client = factory.createClient(options);
}
function map(obj) {
return obj;
/*
if (!obj || typeof obj !== 'object') {
return obj;
}
var data = {};
for (var i in obj) {
if (obj.hasOwnProperty(i) && typeof obj[i] !== 'function'
&& typeof obj[i] !== 'object') {
if (i === 'newListener' || i === 'delimiter' || i === 'wildcard') {
// Skip properties from the base class
continue;
}
data[i] = obj[i];
}
}
return data;
*/
}
/**
* List all storage service containers.
* @param {Function} callback Callback function; parameters: err - error message, containers - object holding all containers.
*/
StorageService.prototype.getContainers = function (cb) {
this.client.getContainers(function (err, containers) {
if (err) {
cb(err, containers);
} else {
cb(err, containers.map(function (c) {
return map(c);
}));
}
});
};
/**
* Create a new storage service container. Other option properties depend on the provider.
*
* @options {Object} options The options to create a provider; see below;
* @prop {Object} connector <!-- WHAT IS THIS? -->
* @prop {String} provider Storage service provider. Use 'filesystem' for local file system. Other supported values are: 'amazon', 'rackspace', 'azure', and 'openstack'.
* @prop {String} root With 'filesystem' provider, the path to the root of storage directory.
* @prop {String}
* @param {Function} callback Callback function.
*/
StorageService.prototype.createContainer = function (options, cb) {
options = options || {};
if ('object' === typeof options && !(options instanceof storage.Container)) {
var Container = factory.getProvider(this.provider).Container;
options = new Container(this.client, options);
}
return this.client.createContainer(options, function (err, container) {
return cb(err, map(container));
});
};
/**
* Destroy an existing storage service container.
* @param {Object} container Container object.
* @param {Function} callback Callback function.
*/
StorageService.prototype.destroyContainer = function (container, cb) {
return this.client.destroyContainer(container, cb);
};
/**
* Look up a container by name.
* @param {Object} container Container object.
* @param {Function} callback Callback function.
*/
StorageService.prototype.getContainer = function (container, cb) {
return this.client.getContainer(container, function (err, container) {
return cb(err, map(container));
});
};
/**
* Get the stream for uploading
* @param {Object} container Container object.
* @param {String} file IS THIS A FILE?
* @options options See below.
* @param callback Callback function
*/
StorageService.prototype.uploadStream = function (container, file, options, cb) {
if (!cb && typeof options === 'function') {
cb = options;
options = {};
}
options = options || {};
if (container) {
options.container = container;
}
if (file) {
options.remote = file;
}
return this.client.upload(options, cb);
};
/**
* Get the stream for downloading.
* @param {Object} container Container object.
* @param {String} file Path to file.
* @options {Object} options See below. <!-- What are the options -->
* @param {Function} callback Callback function
*/
StorageService.prototype.downloadStream = function (container, file, options, cb) {
if (!cb && typeof options === 'function') {
cb = options;
options = {};
}
options = options || {};
if (container) {
options.container = container;
}
if (file) {
options.remote = file;
}
return this.client.download(options, cb);
};
/**
* List all files within the given container.
* @param {Object} container Container object.
* @param {Function} download
* @param {Function} callback Callback function
*/
StorageService.prototype.getFiles = function (container, download, cb) {
return this.client.getFiles(container, download, function (err, files) {
if (err) {
cb(err, files);
} else {
cb(err, files.map(function (f) {
return map(f);
}));
}
});
};
StorageService.prototype.getFile = function (container, file, cb) {
return this.client.getFile(container, file, function (err, f) {
return cb(err, map(f));
});
};
StorageService.prototype.removeFile = function (container, file, cb) {
return this.client.removeFile(container, file, cb);
};
StorageService.prototype.upload = function (req, res, cb) {
return handler.upload(this.client, req, res, req.params.container, cb);
};
StorageService.prototype.download = function (container, file, res, cb) {
return handler.download(this.client, null, res, container, file, cb);
};
StorageService.modelName = 'storage';
StorageService.prototype.getContainers.shared = true;
StorageService.prototype.getContainers.accepts = [];
StorageService.prototype.getContainers.returns = {arg: 'containers', type: 'array', root: true};
StorageService.prototype.getContainers.http =
{verb: 'get', path: '/'};
StorageService.prototype.getContainer.shared = true;
StorageService.prototype.getContainer.accepts = [
{arg: 'container', type: 'string'}
];
StorageService.prototype.getContainer.returns = {arg: 'container', type: 'object', root: true};
StorageService.prototype.getContainer.http =
{verb: 'get', path: '/:container'};
StorageService.prototype.createContainer.shared = true;
StorageService.prototype.createContainer.accepts = [
{arg: 'options', type: 'object', http: {source: 'body'}}
];
StorageService.prototype.createContainer.returns = {arg: 'container', type: 'object', root: true};
StorageService.prototype.createContainer.http =
{verb: 'post', path: '/'};
StorageService.prototype.destroyContainer.shared = true;
StorageService.prototype.destroyContainer.accepts = [
{arg: 'container', type: 'string'}
];
StorageService.prototype.destroyContainer.returns = {};
StorageService.prototype.destroyContainer.http =
{verb: 'delete', path: '/:container'};
StorageService.prototype.getFiles.shared = true;
StorageService.prototype.getFiles.accepts = [
{arg: 'container', type: 'string'}
];
StorageService.prototype.getFiles.returns = {arg: 'files', type: 'array', root: true};
StorageService.prototype.getFiles.http =
{verb: 'get', path: '/:container/files'};
StorageService.prototype.getFile.shared = true;
StorageService.prototype.getFile.accepts = [
{arg: 'container', type: 'string'},
{arg: 'file', type: 'string'}
];
StorageService.prototype.getFile.returns = {arg: 'file', type: 'object', root: true};
StorageService.prototype.getFile.http =
{verb: 'get', path: '/:container/files/:file'};
StorageService.prototype.removeFile.shared = true;
StorageService.prototype.removeFile.accepts = [
{arg: 'container', type: 'string'},
{arg: 'file', type: 'string'}
];
StorageService.prototype.removeFile.returns = {};
StorageService.prototype.removeFile.http =
{verb: 'delete', path: '/:container/files/:file'};
StorageService.prototype.upload.shared = true;
StorageService.prototype.upload.accepts = [
{arg: 'req', type: 'object', 'http': {source: 'req'}},
{arg: 'res', type: 'object', 'http': {source: 'res'}}
];
StorageService.prototype.upload.returns = {arg: 'result', type: 'object'};
StorageService.prototype.upload.http =
{verb: 'post', path: '/:container/upload'};
StorageService.prototype.download.shared = true;
StorageService.prototype.download.accepts = [
{arg: 'container', type: 'string', 'http': {source: 'path'}},
{arg: 'file', type: 'string', 'http': {source: 'path'}},
{arg: 'res', type: 'object', 'http': {source: 'res'}}
];
StorageService.prototype.download.http =
{verb: 'get', path: '/:container/download/:file'};

View File

@ -2,25 +2,28 @@
"name": "loopback-storage-service",
"description": "Loopback Storage Service",
"version": "1.0.0",
"main": "lib/index.js",
"main": "index.js",
"scripts": {
"test": "./node_modules/.bin/mocha --timeout 30000 test/*test.js"
},
"dependencies": {
"pkgcloud": "~0.8.14",
"async": "~0.2.9"
"pkgcloud": "~0.9.4",
"async": "~0.2.10"
},
"devDependencies": {
"express": "~3.4.0",
"loopback": "1.x.x",
"formidable": "~1.0.14",
"mocha": "~1.14.0",
"supertest": "~0.8.1",
"mocha": "~1.18.2",
"supertest": "~0.10.0",
"mkdirp": "~0.3.5"
},
"repository": {
"type": "git",
"url": "https://github.com/strongloop/loopback-storage-service.git"
},
"license": "MIT"
"license": {
"name": "Dual Artistic-2.0/StrongLoop",
"url": "https://github.com/strongloop/loopback-strorage-service/blob/master/LICENSE"
}
}

View File

@ -3,145 +3,159 @@ var FileSystemProvider = require('../lib/providers/filesystem/index.js').Client;
var assert = require('assert');
var path = require('path');
function verifyMetadata(fileOrContainer, name) {
assert(fileOrContainer.getMetadata());
assert.equal(fileOrContainer.getMetadata().name, name);
assert(fileOrContainer.getMetadata().uid === undefined);
assert(fileOrContainer.getMetadata().gid === undefined);
assert(fileOrContainer.getMetadata().atime);
assert(fileOrContainer.getMetadata().ctime);
assert(fileOrContainer.getMetadata().mtime);
assert.equal(typeof fileOrContainer.getMetadata().size, 'number');
}
describe('FileSystem based storage provider', function () {
describe('container apis', function () {
var client = null;
it('should require an existing directory as the root', function (done) {
client = new FileSystemProvider({root: path.join(__dirname, 'storage')});
process.nextTick(done);
});
it('should complain if the root directory doesn\'t exist', function (done) {
try {
client = new FileSystemProvider({root: path.join(__dirname, '_storage')});
process.nextTick(done.bind(null, 'Error'));
} catch (err) {
// Should be here
process.nextTick(done);
}
});
it('should return an empty list of containers', function (done) {
client.getContainers(function (err, containers) {
assert(!err);
assert.equal(0, containers.length);
done(err, containers);
});
});
it('should create a new container', function (done) {
client.createContainer({name: 'c1'}, function (err, container) {
assert(!err);
done(err, container);
});
});
it('should get a container c1', function (done) {
client.getContainer('c1', function (err, container) {
assert(!err);
done(err, container);
});
});
it('should not get a container c2', function (done) {
client.getContainer('c2', function (err, container) {
assert(err);
done(null, container);
});
});
it('should return one container', function (done) {
client.getContainers(function (err, containers) {
assert(!err);
assert.equal(1, containers.length);
done(err, containers);
});
});
it('should destroy a container c1', function (done) {
client.destroyContainer('c1', function (err, container) {
assert(!err);
done(err, container);
});
});
it('should not get a container c1 after destroy', function (done) {
client.getContainer('c1', function (err, container) {
assert(err);
done(null, container);
});
});
describe('container apis', function () {
var client = null;
it('should require an existing directory as the root', function (done) {
client = new FileSystemProvider({root: path.join(__dirname, 'storage')});
process.nextTick(done);
});
describe('file apis', function () {
var fs = require('fs');
var client = new FileSystemProvider({root: path.join(__dirname, 'storage')});
it('should create a new container', function (done) {
client.createContainer({name: 'c1'}, function (err, container) {
assert(!err);
done(err, container);
});
});
it('should upload a file', function (done) {
var writer = client.upload({container: 'c1', remote: 'f1.txt'});
fs.createReadStream(path.join(__dirname, 'files/f1.txt')).pipe(writer);
writer.on('finish', done);
writer.on('error', done);
});
it('should download a file', function (done) {
var reader = client.download({
container: 'c1',
remote: 'f1.txt'
});
reader.pipe(fs.createWriteStream(path.join(__dirname, 'files/f1_downloaded.txt')));
reader.on('end', done);
reader.on('error', done);
});
it('should get files for a container', function (done) {
client.getFiles('c1', function (err, files) {
assert(!err);
assert.equal(1, files.length);
done(err, files);
});
});
it('should get a file', function (done) {
client.getFile('c1', 'f1.txt', function (err, f) {
assert(!err);
assert.ok(f);
done(err, f);
});
});
it('should remove a file', function (done) {
client.removeFile('c1', 'f1.txt', function (err) {
assert(!err);
done(err);
});
});
it('should get no files from a container', function (done) {
client.getFiles('c1', function (err, files) {
assert(!err);
assert.equal(0, files.length);
done(err, files);
});
});
it('should destroy a container c1', function (done) {
client.destroyContainer('c1', function (err, container) {
// console.error(err);
assert(!err);
done(err, container);
});
});
it('should complain if the root directory doesn\'t exist', function (done) {
try {
client = new FileSystemProvider({root: path.join(__dirname, '_storage')});
process.nextTick(done.bind(null, 'Error'));
} catch (err) {
// Should be here
process.nextTick(done);
}
});
it('should return an empty list of containers', function (done) {
client.getContainers(function (err, containers) {
assert(!err);
assert.equal(0, containers.length);
done(err, containers);
});
});
it('should create a new container', function (done) {
client.createContainer({name: 'c1'}, function (err, container) {
assert(!err);
verifyMetadata(container, 'c1');
done(err, container);
});
});
it('should get a container c1', function (done) {
client.getContainer('c1', function (err, container) {
assert(!err);
verifyMetadata(container, 'c1');
done(err, container);
});
});
it('should not get a container c2', function (done) {
client.getContainer('c2', function (err, container) {
assert(err);
done(null, container);
});
});
it('should return one container', function (done) {
client.getContainers(function (err, containers) {
assert(!err);
assert.equal(1, containers.length);
done(err, containers);
});
});
it('should destroy a container c1', function (done) {
client.destroyContainer('c1', function (err, container) {
assert(!err);
done(err, container);
});
});
it('should not get a container c1 after destroy', function (done) {
client.getContainer('c1', function (err, container) {
assert(err);
done(null, container);
});
});
});
describe('file apis', function () {
var fs = require('fs');
var client = new FileSystemProvider({root: path.join(__dirname, 'storage')});
it('should create a new container', function (done) {
client.createContainer({name: 'c1'}, function (err, container) {
assert(!err);
done(err, container);
});
});
it('should upload a file', function (done) {
var writer = client.upload({container: 'c1', remote: 'f1.txt'});
fs.createReadStream(path.join(__dirname, 'files/f1.txt')).pipe(writer);
writer.on('finish', done);
writer.on('error', done);
});
it('should download a file', function (done) {
var reader = client.download({
container: 'c1',
remote: 'f1.txt'
});
reader.pipe(fs.createWriteStream(path.join(__dirname, 'files/f1_downloaded.txt')));
reader.on('end', done);
reader.on('error', done);
});
it('should get files for a container', function (done) {
client.getFiles('c1', function (err, files) {
assert(!err);
assert.equal(1, files.length);
done(err, files);
});
});
it('should get a file', function (done) {
client.getFile('c1', 'f1.txt', function (err, f) {
assert(!err);
assert.ok(f);
verifyMetadata(f, 'f1.txt');
done(err, f);
});
});
it('should remove a file', function (done) {
client.removeFile('c1', 'f1.txt', function (err) {
assert(!err);
done(err);
});
});
it('should get no files from a container', function (done) {
client.getFiles('c1', function (err, files) {
assert(!err);
assert.equal(0, files.length);
done(err, files);
});
});
it('should destroy a container c1', function (done) {
client.destroyContainer('c1', function (err, container) {
// console.error(err);
assert(!err);
done(err, container);
});
});
});
});

1
test/images/album1/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
test.jpg

View File

@ -1,4 +1,4 @@
var StorageService = require('../lib/index.js');
var StorageService = require('../lib/storage-service.js');
var assert = require('assert');
var path = require('path');
@ -7,124 +7,127 @@ var storageService = new StorageService({root: path.join(__dirname, 'storage'),
describe('Storage service', function () {
describe('container apis', function () {
describe('container apis', function () {
it('should return an empty list of containers', function (done) {
storageService.getContainers(function (err, containers) {
assert(!err);
assert.equal(0, containers.length);
done(err, containers);
});
});
it('should create a new container', function (done) {
storageService.createContainer({name: 'c1'}, function (err, container) {
assert(!err);
done(err, container);
});
});
it('should get a container c1', function (done) {
storageService.getContainer('c1', function (err, container) {
assert(!err);
done(err, container);
});
});
it('should not get a container c2', function (done) {
storageService.getContainer('c2', function (err, container) {
assert(err);
done(null, container);
});
});
it('should return one container', function (done) {
storageService.getContainers(function (err, containers) {
assert(!err);
assert.equal(1, containers.length);
done(err, containers);
});
});
it('should destroy a container c1', function (done) {
storageService.destroyContainer('c1', function (err, container) {
assert(!err);
done(err, container);
});
});
it('should not get a container c1 after destroy', function (done) {
storageService.getContainer('c1', function (err, container) {
assert(err);
done(null, container);
});
});
it('should return an empty list of containers', function (done) {
storageService.getContainers(function (err, containers) {
assert(!err);
assert.equal(0, containers.length);
done(err, containers);
});
});
describe('file apis', function () {
var fs = require('fs');
it('should create a new container', function (done) {
storageService.createContainer({name: 'c1'}, function (err, container) {
assert(!err);
done(err, container);
});
});
it('should upload a file', function (done) {
var writer = storageService.uploadStream('c1', 'f1.txt');
fs.createReadStream(path.join(__dirname, 'files/f1.txt')).pipe(writer);
writer.on('finish', done);
writer.on('error', done);
});
it('should download a file', function (done) {
var reader = storageService.downloadStream('c1','f1.txt');
reader.pipe(fs.createWriteStream(path.join(__dirname, 'files/f1_downloaded.txt')));
reader.on('end', done);
reader.on('error', done);
});
it('should get files for a container', function (done) {
storageService.getFiles('c1', function (err, files) {
assert(!err);
assert.equal(1, files.length);
done(err, files);
});
});
it('should get a file', function (done) {
storageService.getFile('c1', 'f1.txt', function (err, f) {
assert(!err);
assert.ok(f);
done(err, f);
});
});
it('should remove a file', function (done) {
storageService.removeFile('c1', 'f1.txt', function (err) {
assert(!err);
done(err);
});
});
it('should get no files from a container', function (done) {
storageService.getFiles('c1', function (err, files) {
assert(!err);
assert.equal(0, files.length);
done(err, files);
});
});
it('should destroy a container c1', function (done) {
storageService.destroyContainer('c1', function (err, container) {
// console.error(err);
assert(!err);
done(err, container);
});
});
it('should create a new container', function (done) {
storageService.createContainer({name: 'c1'}, function (err, container) {
assert(!err);
assert(container.getMetadata());
done(err, container);
});
});
it('should get a container c1', function (done) {
storageService.getContainer('c1', function (err, container) {
assert(!err);
assert(container.getMetadata());
done(err, container);
});
});
it('should not get a container c2', function (done) {
storageService.getContainer('c2', function (err, container) {
assert(err);
done(null, container);
});
});
it('should return one container', function (done) {
storageService.getContainers(function (err, containers) {
assert(!err);
assert.equal(1, containers.length);
done(err, containers);
});
});
it('should destroy a container c1', function (done) {
storageService.destroyContainer('c1', function (err, container) {
assert(!err);
done(err, container);
});
});
it('should not get a container c1 after destroy', function (done) {
storageService.getContainer('c1', function (err, container) {
assert(err);
done(null, container);
});
});
});
describe('file apis', function () {
var fs = require('fs');
it('should create a new container', function (done) {
storageService.createContainer({name: 'c1'}, function (err, container) {
assert(!err);
done(err, container);
});
});
it('should upload a file', function (done) {
var writer = storageService.uploadStream('c1', 'f1.txt');
fs.createReadStream(path.join(__dirname, 'files/f1.txt')).pipe(writer);
writer.on('finish', done);
writer.on('error', done);
});
it('should download a file', function (done) {
var reader = storageService.downloadStream('c1', 'f1.txt');
reader.pipe(fs.createWriteStream(path.join(__dirname, 'files/f1_downloaded.txt')));
reader.on('end', done);
reader.on('error', done);
});
it('should get files for a container', function (done) {
storageService.getFiles('c1', function (err, files) {
assert(!err);
assert.equal(1, files.length);
done(err, files);
});
});
it('should get a file', function (done) {
storageService.getFile('c1', 'f1.txt', function (err, f) {
assert(!err);
assert.ok(f);
assert(f.getMetadata());
done(err, f);
});
});
it('should remove a file', function (done) {
storageService.removeFile('c1', 'f1.txt', function (err) {
assert(!err);
done(err);
});
});
it('should get no files from a container', function (done) {
storageService.getFiles('c1', function (err, files) {
assert(!err);
assert.equal(0, files.length);
done(err, files);
});
});
it('should destroy a container c1', function (done) {
storageService.destroyContainer('c1', function (err, container) {
// console.error(err);
assert(!err);
done(err, container);
});
});
});
});

View File

@ -0,0 +1,199 @@
var request = require('supertest');
var loopback = require('loopback');
var assert = require('assert');
var app = loopback();
var path = require('path');
// expose a rest api
app.use(loopback.rest());
var ds = loopback.createDataSource({
connector: require('../lib/storage-connector'),
provider: 'filesystem',
root: path.join(__dirname, 'images')
});
var Container = ds.createModel('container');
app.model(Container);
/*!
* Verify that the JSON response has the correct metadata properties.
* Please note the metadata vary by storage providers. This test assumes
* the 'filesystem' provider.
*
* @param {String} containerOrFile The container/file object
* @param {String} [name] The name to be checked if not undefined
*/
function verifyMetadata(containerOrFile, name) {
assert(containerOrFile);
// Name
if (name) {
assert.equal(containerOrFile.name, name);
}
// No sensitive information
assert(containerOrFile.uid === undefined);
assert(containerOrFile.gid === undefined);
// Timestamps
assert(containerOrFile.atime);
assert(containerOrFile.ctime);
assert(containerOrFile.mtime);
// Size
assert.equal(typeof containerOrFile.size, 'number');
}
describe('storage service', function () {
var server = null;
before(function (done) {
server = app.listen(3000, function () {
done();
});
});
after(function () {
server.close();
});
it('should create a container', function (done) {
request('http://localhost:3000')
.post('/containers')
.send({name: 'test-container'})
.set('Accept', 'application/json')
.set('Content-Type', 'application/json')
.expect('Content-Type', /json/)
.expect(200, function (err, res) {
verifyMetadata(res.body, 'test-container');
done();
});
});
it('should get a container', function (done) {
request('http://localhost:3000')
.get('/containers/test-container')
.set('Accept', 'application/json')
.expect('Content-Type', /json/)
.expect(200, function (err, res) {
verifyMetadata(res.body, 'test-container');
done();
});
});
it('should list containers', function (done) {
request('http://localhost:3000')
.get('/containers')
.set('Accept', 'application/json')
.expect('Content-Type', /json/)
.expect(200, function (err, res) {
assert(Array.isArray(res.body));
assert.equal(res.body.length, 2);
res.body.forEach(function(c) {
verifyMetadata(c);
});
done();
});
});
it('should delete a container', function (done) {
request('http://localhost:3000')
.del('/containers/test-container')
.set('Accept', 'application/json')
.expect('Content-Type', /json/)
.expect(200, function (err, res) {
done();
});
});
it('should list containers after delete', function (done) {
request('http://localhost:3000')
.get('/containers')
.set('Accept', 'application/json')
.expect('Content-Type', /json/)
.expect(200, function (err, res) {
assert(Array.isArray(res.body));
assert.equal(res.body.length, 1);
done();
});
});
it('should list files', function (done) {
request('http://localhost:3000')
.get('/containers/album1/files')
.set('Accept', 'application/json')
.expect('Content-Type', /json/)
.expect(200, function (err, res) {
assert(Array.isArray(res.body));
res.body.forEach(function(f) {
verifyMetadata(f);
});
done();
});
});
it('uploads files', function (done) {
request('http://localhost:3000')
.post('/containers/album1/upload')
.attach('image', path.join(__dirname, '../example/test.jpg'))
.set('Accept', 'application/json')
.expect('Content-Type', /json/)
.expect(200, function (err, res) {
assert.deepEqual(res.body, {"result": {"files": {"image": [
{"container": "album1", "name": "test.jpg", "type": "image/jpeg"}
]}, "fields": {}}});
done();
});
});
it('should get file by name', function (done) {
request('http://localhost:3000')
.get('/containers/album1/files/test.jpg')
.set('Accept', 'application/json')
.expect('Content-Type', /json/)
.expect(200, function (err, res) {
verifyMetadata(res.body, 'test.jpg');
done();
});
});
it('downloads files', function (done) {
request('http://localhost:3000')
.get('/containers/album1/download/test.jpg')
.expect('Content-Type', 'image/jpeg')
.expect(200, function (err, res) {
done();
});
});
it('should delete a file', function (done) {
request('http://localhost:3000')
.del('/containers/album1/files/test.jpg')
.set('Accept', 'application/json')
.expect('Content-Type', /json/)
.expect(200, function (err, res) {
done();
});
});
it('reports errors if it fails to find the file to download', function (done) {
request('http://localhost:3000')
.get('/containers/album1/download/test_not_exist.jpg')
.expect('Content-Type', /json/)
.expect(500, function (err, res) {
assert(res.body.error);
done();
});
});
});