Merge branch 'release/1.0.0' into production
This commit is contained in:
commit
50cd49dd9a
506
LICENSE
506
LICENSE
|
@ -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.
|
||||
|
|
142
README.md
142
README.md
|
@ -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',
|
||||
LoopBack exposes the APIs using a model that is attached to a data source configured
|
||||
with the loopback-storage-service connector.
|
||||
|
||||
//
|
||||
// ... Provider specific credentials
|
||||
//
|
||||
var ds = loopback.createDataSource({
|
||||
connector: require('loopback-storage-service'),
|
||||
provider: 'filesystem',
|
||||
root: path.join(__dirname, 'storage')
|
||||
});
|
||||
```
|
||||
|
||||
Each compute provider takes different credentials to authenticate; these details about each specific provider can be found below:
|
||||
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');
|
||||
## REST APIs
|
||||
|
||||
var storageService = storage({ /* ... */ });
|
||||
- GET /api/containers
|
||||
|
||||
fs.createReadStream('a-file.txt').pipe(storageService.uploadStream('a-container','remote-file-name.txt'));
|
||||
```
|
||||
List all containers
|
||||
|
||||
### Download a File
|
||||
``` js
|
||||
var storage = require('loopback-storage-service'),
|
||||
fs = require('fs');
|
||||
- GET /api/containers/:container
|
||||
|
||||
var storageService = storage({ /* ... */ });
|
||||
Get information about a container by name
|
||||
|
||||
storageService.downloadStream({
|
||||
container: 'a-container',
|
||||
remote: 'remote-file-name.txt'
|
||||
}).pipe(fs.createWriteStream('a-file.txt'));
|
||||
```
|
||||
- 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
|
|
@ -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"
|
||||
]
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
providers-private.json
|
|
@ -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);
|
||||
|
|
@ -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'));
|
116
example/app.js
116
example/app.js
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
/*
|
||||
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({
|
||||
var ds = loopback.createDataSource({
|
||||
connector: require('../index'),
|
||||
provider: 'filesystem',
|
||||
root: path.join(__dirname, 'storage')
|
||||
});
|
||||
|
||||
// Container
|
||||
var container = ds.createModel('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.model(container);
|
||||
|
||||
/*
|
||||
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'));
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -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
|
@ -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();
|
||||
});
|
||||
|
||||
});
|
|
@ -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>
|
|
@ -1,5 +1,5 @@
|
|||
var StorageService = require('../');
|
||||
|
||||
var StorageService = require('../').StorageService;
|
||||
var providers = require('./providers.json');
|
||||
var express = require('express');
|
||||
var app = express();
|
||||
|
||||
|
@ -16,8 +16,8 @@ app.configure(function () {
|
|||
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) {
|
||||
|
@ -68,7 +68,7 @@ app.get('/download/:container', function (req, res, next) {
|
|||
});
|
||||
|
||||
app.get('/download/:container/:file', function (req, res, next) {
|
||||
handler.download(req, res, function (err, result) {
|
||||
handler.download(req.params.container, req.params.file, res, function (err, result) {
|
||||
if (err) {
|
||||
res.send(500, err);
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
var StorageService = require('../');
|
||||
var StorageService = require('../').StorageService;
|
||||
|
||||
var express = require('express');
|
||||
var app = express();
|
||||
|
@ -67,7 +67,7 @@ app.get('/download/:container', function (req, res, next) {
|
|||
});
|
||||
|
||||
app.get('/download/:container/:file', function (req, res, next) {
|
||||
handler.download(req, res, function (err, result) {
|
||||
handler.download(req.params.container, req.params.file, res, function (err, result) {
|
||||
if (err) {
|
||||
res.send(500, err);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
var StorageConnector = require('./lib/storage-connector');
|
||||
StorageConnector.StorageService = require('./lib/storage-service');
|
||||
|
||||
module.exports = StorageConnector;
|
||||
|
|
@ -1,3 +1,65 @@
|
|||
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
|
||||
|
@ -15,7 +77,7 @@ function createClient(options) {
|
|||
// Fall back to pkgcloud
|
||||
handler = require('pkgcloud').storage;
|
||||
}
|
||||
|
||||
patchContainerAndFileClass(provider);
|
||||
return handler.createClient(options);
|
||||
}
|
||||
|
||||
|
|
150
lib/index.js
150
lib/index.js
|
@ -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'}
|
||||
];
|
|
@ -5,7 +5,7 @@ exports.Container = Container;
|
|||
|
||||
function Container(client, details) {
|
||||
base.Container.call(this, client, details);
|
||||
};
|
||||
}
|
||||
|
||||
util.inherits(Container, base.Container);
|
||||
|
||||
|
@ -15,4 +15,4 @@ Container.prototype._setProperties = function(details) {
|
|||
this[k] = details[k];
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
|
@ -5,7 +5,7 @@ exports.File = File;
|
|||
|
||||
function File(client, details) {
|
||||
base.File.call(this, client, details);
|
||||
};
|
||||
}
|
||||
|
||||
util.inherits(File, base.File);
|
||||
|
||||
|
@ -15,4 +15,4 @@ File.prototype._setProperties = function(details) {
|
|||
this[k] = details[k];
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
|
@ -8,6 +8,8 @@ var fs = require('fs'),
|
|||
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;
|
||||
|
@ -20,11 +22,11 @@ function FileSystemProvider(options) {
|
|||
this.root = options.root;
|
||||
var exists = fs.existsSync(this.root);
|
||||
if (!exists) {
|
||||
throw new Error('Path does not exist: ' + this.root);
|
||||
throw new Error('FileSystemProvider: Path does not exist: ' + this.root);
|
||||
}
|
||||
var stat = fs.statSync(this.root);
|
||||
if (!stat.isDirectory()) {
|
||||
throw new Error('Invalid directory: ' + this.root);
|
||||
throw new Error('FileSystemProvider: Invalid directory: ' + this.root);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -34,7 +36,7 @@ function validateName(name, cb) {
|
|||
if (!name) {
|
||||
cb && process.nextTick(cb.bind(null, new Error('Invalid name: ' + name)));
|
||||
if (!cb) {
|
||||
console.error('Invalid name: ', name);
|
||||
console.error('FileSystemProvider: Invalid name: ', name);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
@ -42,22 +44,40 @@ function validateName(name, cb) {
|
|||
if (match && match.index === 0 && match[0].length === name.length) {
|
||||
return true;
|
||||
} else {
|
||||
cb && process.nextTick(cb.bind(null, new Error('Invalid name: ' + name)));
|
||||
cb && process.nextTick(cb.bind(null,
|
||||
new Error('FileSystemProvider: Invalid name: ' + name)));
|
||||
if (!cb) {
|
||||
console.error('Invalid name: ', name);
|
||||
console.error('FileSystemProvider: Invalid name: ', name);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Container related functions
|
||||
/*!
|
||||
* 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(null, path.join(self.root, f)));
|
||||
tasks.push(fs.stat.bind(fs, path.join(self.root, f)));
|
||||
});
|
||||
async.parallel(tasks, function (err, stats) {
|
||||
if (err) {
|
||||
|
@ -67,9 +87,7 @@ FileSystemProvider.prototype.getContainers = function (cb) {
|
|||
if (stat.isDirectory()) {
|
||||
var name = files[index];
|
||||
var props = {name: name};
|
||||
for (var p in stat) {
|
||||
props[p] = stat[p];
|
||||
}
|
||||
populateMetadata(stat, props);
|
||||
var container = new Container(self, props);
|
||||
containers.push(container);
|
||||
}
|
||||
|
@ -78,15 +96,27 @@ FileSystemProvider.prototype.getContainers = function (cb) {
|
|||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
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 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;
|
||||
|
@ -95,7 +125,7 @@ FileSystemProvider.prototype.destroyContainer = function (containerName, cb) {
|
|||
fs.readdir(dir, function (err, files) {
|
||||
var tasks = [];
|
||||
files.forEach(function (f) {
|
||||
tasks.push(fs.unlink.bind(null, path.join(dir, f)));
|
||||
tasks.push(fs.unlink.bind(fs, path.join(dir, f)));
|
||||
});
|
||||
async.parallel(tasks, function (err) {
|
||||
if (err) {
|
||||
|
@ -105,7 +135,7 @@ FileSystemProvider.prototype.destroyContainer = function (containerName, cb) {
|
|||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
FileSystemProvider.prototype.getContainer = function (containerName, cb) {
|
||||
var self = this;
|
||||
|
@ -115,15 +145,12 @@ FileSystemProvider.prototype.getContainer = function (containerName, cb) {
|
|||
var container = null;
|
||||
if (!err) {
|
||||
var props = {name: containerName};
|
||||
for (var p in stat) {
|
||||
props[p] = stat[p];
|
||||
}
|
||||
populateMetadata(stat, props);
|
||||
container = new Container(self, props);
|
||||
}
|
||||
cb && cb(err, container);
|
||||
});
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
// File related functions
|
||||
FileSystemProvider.prototype.upload = function (options, cb) {
|
||||
|
@ -133,12 +160,17 @@ FileSystemProvider.prototype.upload = function (options, cb) {
|
|||
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;
|
||||
|
@ -151,9 +183,12 @@ FileSystemProvider.prototype.download = function (options, cb) {
|
|||
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)) {
|
||||
|
@ -167,7 +202,7 @@ FileSystemProvider.prototype.getFiles = function (container, download, cb) {
|
|||
var files = [];
|
||||
var tasks = [];
|
||||
entries.forEach(function (f) {
|
||||
tasks.push(fs.stat.bind(null, path.join(dir, f)));
|
||||
tasks.push(fs.stat.bind(fs, path.join(dir, f)));
|
||||
});
|
||||
async.parallel(tasks, function (err, stats) {
|
||||
if (err) {
|
||||
|
@ -176,9 +211,7 @@ FileSystemProvider.prototype.getFiles = function (container, download, cb) {
|
|||
stats.forEach(function (stat, index) {
|
||||
if (stat.isFile()) {
|
||||
var props = {container: container, name: entries[index]};
|
||||
for (var p in stat) {
|
||||
props[p] = stat[p];
|
||||
}
|
||||
populateMetadata(stat, props);
|
||||
var file = new File(self, props);
|
||||
files.push(file);
|
||||
}
|
||||
|
@ -187,8 +220,7 @@ FileSystemProvider.prototype.getFiles = function (container, download, cb) {
|
|||
}
|
||||
});
|
||||
});
|
||||
|
||||
}
|
||||
};
|
||||
|
||||
FileSystemProvider.prototype.getFile = function (container, file, cb) {
|
||||
var self = this;
|
||||
|
@ -199,14 +231,18 @@ FileSystemProvider.prototype.getFile = function (container, file, cb) {
|
|||
var f = null;
|
||||
if (!err) {
|
||||
var props = {container: container, name: file};
|
||||
for (var p in stat) {
|
||||
props[p] = stat[p];
|
||||
}
|
||||
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;
|
||||
|
@ -214,4 +250,4 @@ FileSystemProvider.prototype.removeFile = function (container, file, cb) {
|
|||
|
||||
var filePath = path.join(this.root, container, file);
|
||||
fs.unlink(filePath, cb);
|
||||
}
|
||||
};
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
var StorageService = require('./index');
|
||||
var StorageService = require('./storage-service');
|
||||
/**
|
||||
* Export the initialize method to Loopback data
|
||||
* @param dataSource
|
||||
|
@ -11,7 +11,8 @@ exports.initialize = function (dataSource, callback) {
|
|||
dataSource.connector = connector;
|
||||
dataSource.connector.dataSource = dataSource;
|
||||
|
||||
connector.DataAccessObject = function() {};
|
||||
connector.DataAccessObject = function () {
|
||||
};
|
||||
for (var m in StorageService.prototype) {
|
||||
var method = StorageService.prototype[m];
|
||||
if ('function' === typeof method) {
|
||||
|
@ -22,5 +23,6 @@ exports.initialize = function (dataSource, callback) {
|
|||
}
|
||||
}
|
||||
|
||||
connector.define = function(model, properties, settings) {};
|
||||
}
|
||||
connector.define = function (model, properties, settings) {
|
||||
};
|
||||
};
|
||||
|
|
|
@ -4,13 +4,14 @@ 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) {
|
||||
exports.upload = function (provider, req, res, container, cb) {
|
||||
var form = new IncomingForm(this.options);
|
||||
var container = req.params.container;
|
||||
container = container || req.params.container;
|
||||
var fields = {}, files = {};
|
||||
form.handlePart = function (part) {
|
||||
var self = this;
|
||||
|
@ -104,21 +105,22 @@ exports.upload = function (provider, req, res, cb) {
|
|||
/**
|
||||
* 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) {
|
||||
exports.download = function (provider, req, res, container, file, cb) {
|
||||
var reader = provider.download({
|
||||
container: req.params.container,
|
||||
remote: req.params.file
|
||||
container: container || req && req.params.container,
|
||||
remote: file || req && req.params.file
|
||||
});
|
||||
res.type(file);
|
||||
reader.pipe(res);
|
||||
reader.on('error', function (err) {
|
||||
cb && cb(err);
|
||||
});
|
||||
reader.on('end', function(err, result) {
|
||||
cb && cb(err, result);
|
||||
res.type('application/json');
|
||||
res.send(500, { error: err });
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -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'};
|
15
package.json
15
package.json
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,6 +3,17 @@ 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 () {
|
||||
|
@ -33,6 +44,7 @@ describe('FileSystem based storage provider', function () {
|
|||
it('should create a new container', function (done) {
|
||||
client.createContainer({name: 'c1'}, function (err, container) {
|
||||
assert(!err);
|
||||
verifyMetadata(container, 'c1');
|
||||
done(err, container);
|
||||
});
|
||||
});
|
||||
|
@ -40,6 +52,7 @@ describe('FileSystem based storage provider', function () {
|
|||
it('should get a container c1', function (done) {
|
||||
client.getContainer('c1', function (err, container) {
|
||||
assert(!err);
|
||||
verifyMetadata(container, 'c1');
|
||||
done(err, container);
|
||||
});
|
||||
});
|
||||
|
@ -114,6 +127,7 @@ describe('FileSystem based storage provider', function () {
|
|||
client.getFile('c1', 'f1.txt', function (err, f) {
|
||||
assert(!err);
|
||||
assert.ok(f);
|
||||
verifyMetadata(f, 'f1.txt');
|
||||
done(err, f);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
test.jpg
|
|
@ -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');
|
||||
|
@ -20,6 +20,7 @@ describe('Storage service', function () {
|
|||
it('should create a new container', function (done) {
|
||||
storageService.createContainer({name: 'c1'}, function (err, container) {
|
||||
assert(!err);
|
||||
assert(container.getMetadata());
|
||||
done(err, container);
|
||||
});
|
||||
});
|
||||
|
@ -27,6 +28,7 @@ describe('Storage service', function () {
|
|||
it('should get a container c1', function (done) {
|
||||
storageService.getContainer('c1', function (err, container) {
|
||||
assert(!err);
|
||||
assert(container.getMetadata());
|
||||
done(err, container);
|
||||
});
|
||||
});
|
||||
|
@ -97,6 +99,7 @@ describe('Storage service', function () {
|
|||
storageService.getFile('c1', 'f1.txt', function (err, f) {
|
||||
assert(!err);
|
||||
assert.ok(f);
|
||||
assert(f.getMetadata());
|
||||
done(err, f);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
Loading…
Reference in New Issue