commit bdf0949190385277db35100a46b64dc2d682dd25 Author: Jeff Morgan Date: Wed Aug 27 13:45:22 2014 -0700 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000..9bb8fa791c --- /dev/null +++ b/.gitignore @@ -0,0 +1,20 @@ +.DS_Store +bundle +bundle.tar.gz +.demeteorized +dist +node_modules +package +cache +bin + +# Resources +resources/cache +resources/base-images.tar.gz +resources/virtualbox-4.3.12.pkg +resources/boot2docker +resources/node-webkit +resources/mongod +resources/MONGOD_LICENSE + +script/sign.sh diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100755 index 0000000000..45d12a3bca --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,190 @@ +# Contributing to Kitematic + +Thanks for contributing and supporting the Kitematic project. + +Before you file a bug or pull request, please take a comment to read the +following guidelines for more details on how to file a bug report or submit a +pull request to help make the contribution process awesome for +everyone working on this project. + +Following these guidelines helps to communicate that you respect the time of +the developers managing and developing this open source project. In return, +they should reciprocate that respect in addressing your issue or assessing +patches and features. + + +## Table of Contents + + - [Using the Issue Tracker](#using-the-issue-tracker) + - [Bugs Reports](#bug-reports) + - [Feature Requests](#feature-requests) + - [Submitting Pull Requests](#submitting-pull-requests) + - [Code Guidelines](#code-guidelines) + - [License](#license) + + +## Using the Issue Tracker + +The [Issue Tracker](https://github.com/kitematic/kitematic/issues) is +the preferred channel for [Bug Reports](#bug-reports), [Features Requests](#feature-requests) +and [Submitting Pull Requests](#submitting-pull-requests), but please respect the following +restrictions: + +* Please don't use the issue tracker for personal support requests. Our +[HipChat room](https://www.hipchat.com/giAT9Fqb5) is a better place to get help. + +## Bug Reports + +A bug is a _demonstrable problem_ that is caused by the code in the repository. +Good bug reports are extremely helpful, so thanks! + +Guidelines for bug reports: + +1. **Use the GitHub issue search** — check if the issue has already been + reported. + +2. **Check if the issue has been fixed** — try to reproduce it using the + latest `master` or development branch in the repository. + +3. **More details are encouraged** — please give more details on the steps + to reproduce the bug and attach a screenshot of the bug if possible. + +A good bug report shouldn't leave others needing to chase you up for more +information. Please try to be as detailed as possible in your report. What is +your environment? What steps will reproduce the issue? What browser(s) and OS +experience the problem? Do other browsers show the bug differently? What +would you expect to be the outcome? All these details will help people to fix +any potential bugs. + +Example: + +> Short and descriptive example bug report title +> +> A summary of the issue and the browser/OS environment in which it occurs. If +> suitable, include the steps required to reproduce the bug. +> +> 1. This is the first step +> 2. This is the second step +> 3. Further steps, etc. +> +> `` - a link to the screen shot of the bug if possible +> +> Any other information you want to share that is relevant to the issue being +> reported. This might include the lines of code that you have identified as +> causing the bug, and potential solutions (and your opinions on their +> merits). + + + +## Feature Requests + +Feature requests are welcome. But take a moment to find out whether your idea +fits with the scope and aims of the project. A roadmap of the project is kept +on the [Kitematic Roadmap](https://trello.com/b/xea5AHRk/kitematic-roadmap) Trello board. +It's up to *you* to make a strong case to convince the project's developers of +the merits of this feature. Please provide as much detail and context as possible. + + + +## Submitting Pull Requests + +Good pull requests—patches, improvements, new features—are a fantastic +help. They should remain focused in scope and avoid containing unrelated +commits. + +**Please ask first** before embarking on any significant pull request (e.g. +implementing features, refactoring code, porting to a different language), +otherwise you risk spending a lot of time working on something that the +project's developers might not want to merge into the project. + +Please adhere to the [Code Guidelines](#code-guidelines) used throughout the +project (indentation, accurate comments, etc.) and any other requirements +(such as test coverage). + +Adhering to the following process is the best way to get your work +included in the project: + +1. [Fork](http://help.github.com/fork-a-repo/) the project, clone your fork, + and configure the remotes: + + ```bash + # Clone your fork of the repo into the current directory + git clone https://github.com//kitematic.git + # Navigate to the newly cloned directory + cd kitematic + # Assign the original repo to a remote called "upstream" + git remote add upstream https://github.com/kitematic/kitematic.git + ``` + +2. If you cloned a while ago, get the latest changes from upstream: + + ```bash + git checkout master + git pull upstream master + ``` + +3. Create a new topic branch (off the main project development branch) to + contain your feature, change, or fix: + + ```bash + git checkout -b + ``` + +4. Commit your changes in logical chunks. Please adhere to these [git commit + message guidelines](http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html) + or your code is unlikely be merged into the main project. Use Git's + [interactive rebase](https://help.github.com/articles/interactive-rebase) + feature to tidy up your commits before making them public. + +5. Locally merge (or rebase) the upstream development branch into your topic branch: + + ```bash + git pull [--rebase] upstream master + ``` + +6. Push your topic branch up to your fork: + + ```bash + git push origin + ``` + +7. [Open a Pull Request](https://help.github.com/articles/using-pull-requests/) + with a clear title and description against the `master` branch. + + + +## Code Guidelines + +### Javascript + +- Semicolons (in client-side JS) +- 2 spaces (no tabs) +- strict mode +- "Attractive" + +#### Checking Javascript code standards with JSHint + +Run `sh jshint.sh` before committing to ensure your changes follow our coding +standards. Add any exceptions to global variables at the bottom of the +*.jshintrc* file. + +### HTML + +[Adhere to the Code Guide.](http://codeguide.co/#html) + +- Use tags and elements appropriate for an HTML5 doctype (e.g., self-closing tags). +- Use CDNs and HTTPS for third-party JS when possible. We don't use protocol-relative URLs in this case because they break when viewing the page locally via `file://`. +- Use [WAI-ARIA](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA) attributes in documentation examples to promote accessibility. + +### CSS/LESS + +[Adhere to the Code Guide.](http://codeguide.co/#css) + +- When feasible, default color palettes should comply with [WCAG color contrast guidelines](http://www.w3.org/TR/WCAG20/#visual-audio-contrast). +- Except in rare cases, don't remove default `:focus` styles (via e.g. `outline: none;`) without providing alternative styles. See [this A11Y Project post](http://a11yproject.com/posts/never-remove-css-outlines/) for more details. + + + +## License + +By contributing your code, you agree to license your contribution under the [AGPL license](https://github.com/kitematic/kitematic/blob/master/LICENSE). diff --git a/LICENSE b/LICENSE new file mode 100755 index 0000000000..2def0e8831 --- /dev/null +++ b/LICENSE @@ -0,0 +1,661 @@ + GNU AFFERO GENERAL PUBLIC LICENSE + Version 3, 19 November 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU Affero General Public License is a free, copyleft license for +software and other kinds of works, specifically designed to ensure +cooperation with the community in the case of network server software. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +our General Public Licenses are intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + Developers that use our General Public Licenses protect your rights +with two steps: (1) assert copyright on the software, and (2) offer +you this License which gives you legal permission to copy, distribute +and/or modify the software. + + A secondary benefit of defending all users' freedom is that +improvements made in alternate versions of the program, if they +receive widespread use, become available for other developers to +incorporate. Many developers of free software are heartened and +encouraged by the resulting cooperation. However, in the case of +software used on network servers, this result may fail to come about. +The GNU General Public License permits making a modified version and +letting the public access it on a server without ever releasing its +source code to the public. + + The GNU Affero General Public License is designed specifically to +ensure that, in such cases, the modified source code becomes available +to the community. It requires the operator of a network server to +provide the source code of the modified version running there to the +users of that server. Therefore, public use of a modified version, on +a publicly accessible server, gives the public access to the source +code of the modified version. + + An older license, called the Affero General Public License and +published by Affero, was designed to accomplish similar goals. This is +a different license, not a version of the Affero GPL, but Affero has +released a new version of the Affero GPL which permits relicensing under +this license. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU Affero General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Remote Network Interaction; Use with the GNU General Public License. + + Notwithstanding any other provision of this License, if you modify the +Program, your modified version must prominently offer all users +interacting with it remotely through a computer network (if your version +supports such interaction) an opportunity to receive the Corresponding +Source of your version by providing access to the Corresponding Source +from a network server at no charge, through some standard or customary +means of facilitating copying of software. This Corresponding Source +shall include the Corresponding Source for any work covered by version 3 +of the GNU General Public License that is incorporated pursuant to the +following paragraph. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the work with which it is combined will remain governed by version +3 of the GNU General Public License. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU Affero General Public License from time to time. Such new versions +will be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU Affero General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU Affero General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU Affero General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If your software can interact with users remotely through a computer +network, you should also make sure that it provides a way for users to +get its source. For example, if your program is a web application, its +interface could display a "Source" link that leads users to an archive +of the code. There are many ways you could offer source, and different +solutions will be better for different programs; see section 13 for the +specific requirements. + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU AGPL, see +. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100755 index 0000000000..750c0d812a --- /dev/null +++ b/README.md @@ -0,0 +1,90 @@ +# [Kitematic](https://kitematic.com) + +![Kitematic Screenshot](http://kitematic.com/img/screenshot.0c17.png) + + +## Table of Contents + + - [Development](#development) + - [Bugs and Feature Requests](#bugs-and-feature-requests) + - [Documentation](#documentation) + - [Contributing](#contributing) + - [Community](#community) + - [Versioning](#versioning) + - [Creators](#creators) + - [Copyright and License](#copyright-and-license) + +## Development + +- Install any version of Node.js +- Install meteor.js `curl https://install.meteor.com/ | sh`. +- Install meteorite `npm install meteorite -g` +- Install demeteorizer `npm install demeteorizer -g` +- Run ./script/setup.sh to download the binary requirements (things like virtualbox). + +### Running the develoment Server + +- ./script/run.sh + +### Building the Mac OS X Package + +- ./script/bundle.sh # Generates the app bundle under ./bundle +- ./script/dist.sh # Generates the app under ./dist./osx/Kitematic.app + +## Uninstalling + +(This will improve over time.) + +- Remove VirtualBox +- rm /usr/local/bin/boot2docker +- sudo route delete 172.17.0.0/16 192.168.59.103 (disable routing to containers through VM) +- rm -rf ~/Application\ Support/Kitematic (remove app data) +- rm /Library/LaunchAgents/com.kitematic.route.plist (remove launch job that sets up routing to the containers) + +## Bugs and Feature Requests + +Have a bug or a feature request? Please first read the [Issue Guidelines](https://github.com/kitematic/kitematic/blob/master/CONTRIBUTING.md#using-the-issue-tracker) and search for existing and closed issues. If your problem or idea is not addressed yet, [please open a new issue](https://github.com/kitematic/kitematic/issues/new). + +## Documentation + +Kitematic's documentation and other information can be found at [http://kitematic.com/docs](http://kitematic.com/docs). + +## Contributing + +Please read through our [Contributing Guidelines](https://github.com/kitematic/kitematic/blob/master/CONTRIBUTING.md). Included are directions for opening issues, coding standards, and notes on development. + +Development [Roadmap](https://trello.com/b/xea5AHRk/kitematic-roadmap) can be found on our Trello board. + +## Community + +Keep track of development and community news. + +- Follow [@kitematic on Twitter](https://twitter.com/kitematic). +- Check out Kitematic's [Roadmap](https://trello.com/b/xea5AHRk/kite-roadmap) on our Trello board. +- Read and subscribe to [The Official Kitematic Blog](https://kitematic.com/blog). +- Chat with developers using Kitematic in our [HipChat room](http://www.hipchat.com/giAT9Fqb5). + +## Versioning + +For transparency into our release cycle and in striving to maintain backward compatibility, Kitematic is maintained under the [Semantic Versioning Guidelines](http://semver.org/). We'll try very hard adhere to those rules whenever possible. + +## Creators + +**Sean Li** + +- +- + +**Jeffrey Morgan** + +- +- + +**Michael Chiang** + +- +- + +## Copyright and License + +Code released under the [AGPL license](LICENSE). diff --git a/index.html b/index.html new file mode 100644 index 0000000000..850739c226 --- /dev/null +++ b/index.html @@ -0,0 +1,9 @@ + + + + Welcome to Kitematic + + + + + diff --git a/index.js b/index.js new file mode 100644 index 0000000000..0802ae03b1 --- /dev/null +++ b/index.js @@ -0,0 +1,89 @@ +var child_process = require('child_process'); +var net = require('net'); +var os = require('os'); +var fs = require('fs'); +var path = require('path'); + +var freeport = function (callback) { + var server = net.createServer(); + var port = 0; + server.on('listening', function() { + port = server.address().port; + server.close(); + }); + server.on('close', function() { + callback(null, port); + }); + server.listen(0, '127.0.0.1'); +}; + +var start = function (callback) { + if (process.env.NODE_ENV === 'development') { + callback('http://localhost:3000'); + } else { + process.stdout.write('Starting production server\n'); + if (os.platform() === 'darwin') { + var kitePath = path.join(process.env.HOME, 'Library/Application Support/Kitematic/'); + var dataPath = path.join(kitePath, 'data'); + console.log(dataPath); + var bundlePath = path.join(kitePath, 'bundle'); + if (!fs.existsSync(kitePath)) { + fs.mkdirSync(kitePath); + } + if (!fs.existsSync(dataPath)) { + fs.mkdirSync(dataPath); + } + if (!fs.existsSync(bundlePath)) { + fs.mkdirSync(bundlePath); + } + } + + // One for meteor, one for mongo + freeport(function (err, webPort) { + freeport(function(err, mongoPort) { + child_process.exec('kill $(ps aux -e | grep \'DB_PURPOSE=KITEMATIC\' | awk \'{print $2}\')', function (error, stdout, stderr) { + var command = 'DB_PURPOSE=KITEMATIC ' + process.cwd() + '/resources/mongod --bind_ip 127.0.0.1 --dbpath ' + dataPath.replace(' ', '\\ ') + ' --port ' + mongoPort + ' --unixSocketPrefix ' + dataPath.replace(' ', '\\ '); + console.log(command); + var mongoChild = child_process.exec(command, function (error, stdout, stderr) { + console.log(error); + console.log(stdout); + console.log(stderr); + }); + + process.stdout.write(process.cwd()); + var rootUrl = 'http://localhost:' + webPort; + var user_env = process.env; + process.env.ROOT_URL = rootUrl; + process.env.PORT = webPort; + process.env.BIND_IP = '127.0.0.1'; + process.env.DB_PATH = dataPath; + process.env.MONGO_URL = 'mongodb://localhost:' + mongoPort + '/meteor'; + process.argv.splice(2, 0, 'program.json'); + require('./bundle/main.js'); + callback(process.env.ROOT_URL); + }); + }); + }); + } +}; + +start(function (url) { + var gui = require('nw.gui'); + var mainWindow = gui.Window.get(); + gui.App.on('reopen', function () { + mainWindow.show(); + }); + setTimeout(function () { + mainWindow.window.location = url; + mainWindow.on('loaded', function () { + mainWindow.show(); + }); + }, 600); + mainWindow.on('close', function (type) { + this.hide(); + if (type === 'quit') { + this.close(false); + } + console.log('Window Closed.'); + }); +}); diff --git a/meteor/.gitignore b/meteor/.gitignore new file mode 100755 index 0000000000..dca0464da7 --- /dev/null +++ b/meteor/.gitignore @@ -0,0 +1,6 @@ +.DS_Store +mup.json +env_secret.sh +.demeteorized +*.pyc +meteor-normalized.tar.gz diff --git a/meteor/.jshintrc b/meteor/.jshintrc new file mode 100755 index 0000000000..e2c8f682e4 --- /dev/null +++ b/meteor/.jshintrc @@ -0,0 +1,261 @@ +{ + // JSHint Meteor Configuration File + // Match the Meteor Style Guide + // + // By @raix with contributions from @aldeed and @awatson1978 + // Source https://github.com/raix/Meteor-jshintrc + // + // See http://jshint.com/docs/ for more details + + "maxerr" : 50, // {int} Maximum error before stopping + + // Enforcing + "bitwise" : true, // true: Prohibit bitwise operators (&, |, ^, etc.) + "camelcase" : false, // true: Identifiers must be in camelCase + "curly" : true, // true: Require {} for every new block or scope + "eqeqeq" : true, // true: Require triple equals (===) for comparison + "forin" : true, // true: Require filtering for..in loops with obj.hasOwnProperty() + "immed" : false, // true: Require immediate invocations to be wrapped in parens e.g. `(function () { } ());` + "indent" : 2, // {int} Number of spaces to use for indentation + "latedef" : false, // true: Require variables/functions to be defined before being used + "newcap" : false, // true: Require capitalization of all constructor functions e.g. `new F()` + "noarg" : true, // true: Prohibit use of `arguments.caller` and `arguments.callee` + "noempty" : true, // true: Prohibit use of empty blocks + "nonew" : false, // true: Prohibit use of constructors for side-effects (without assignment) + "plusplus" : false, // true: Prohibit use of `++` & `--` + "quotmark" : false, // Quotation mark consistency: + // false : do nothing (default) + // true : ensure whatever is used is consistent + // "single" : require single quotes + // "double" : require double quotes + "undef" : true, // true: Require all non-global variables to be declared (prevents global leaks) + "unused" : true, // true: Require all defined variables be used + "strict" : false, // true: Requires all functions run in ES5 Strict Mode + "trailing" : true, // true: Prohibit trailing whitespaces + "maxparams" : false, // {int} Max number of formal params allowed per function + "maxdepth" : false, // {int} Max depth of nested blocks (within functions) + "maxstatements" : false, // {int} Max number statements per function + "maxcomplexity" : false, // {int} Max cyclomatic complexity per function + "maxlen" : 1000, // {int} Max number of characters per line + + // Relaxing + "asi" : false, // true: Tolerate Automatic Semicolon Insertion (no semicolons) + "boss" : false, // true: Tolerate assignments where comparisons would be expected + "debug" : false, // true: Allow debugger statements e.g. browser breakpoints. + "eqnull" : false, // true: Tolerate use of `== null` + "es5" : false, // true: Allow ES5 syntax (ex: getters and setters) + "esnext" : false, // true: Allow ES.next (ES6) syntax (ex: `const`) + //"moz" : false, // true: Allow Mozilla specific syntax (extends and overrides esnext features) + // (ex: `for each`, multiple try/catch, function expression…) + "evil" : false, // true: Tolerate use of `eval` and `new Function()` + "expr" : false, // true: Tolerate `ExpressionStatement` as Programs + "funcscope" : false, // true: Tolerate defining variables inside control statements" + "globalstrict" : true, // true: Allow global "use strict" (also enables 'strict') + "iterator" : false, // true: Tolerate using the `__iterator__` property + "lastsemic" : false, // true: Tolerate omitting a semicolon for the last statement of a 1-line block + "laxbreak" : false, // true: Tolerate possibly unsafe line breakings + "laxcomma" : false, // true: Tolerate comma-first style coding + "loopfunc" : false, // true: Tolerate functions being defined in loops + "multistr" : false, // true: Tolerate multi-line strings + "proto" : false, // true: Tolerate using the `__proto__` property + "scripturl" : false, // true: Tolerate script-targeted URLs + "smarttabs" : false, // true: Tolerate mixed tabs/spaces when used for alignment + "shadow" : false, // true: Allows re-define variables later in code e.g. `var x=1; x=2;` + "sub" : false, // true: Tolerate using `[]` notation when it can still be expressed in dot notation + "supernew" : false, // true: Tolerate `new function () { ... };` and `new Object;` + "validthis" : false, // true: Tolerate using this in a non-constructor function + + // Environments + "browser" : true, // Web Browser (window, document, etc) + "couch" : false, // CouchDB + "devel" : true, // Development/debugging (alert, confirm, etc) + "dojo" : false, // Dojo Toolkit + "jquery" : false, // jQuery + "mootools" : false, // MooTools + "node" : false, // Node.js + "nonstandard" : false, // Widely adopted globals (escape, unescape, etc) + "prototypejs" : false, // Prototype and Scriptaculous + "rhino" : false, // Rhino + "worker" : false, // Web Workers + "wsh" : false, // Windows Scripting Host + "yui" : false, // Yahoo User Interface + //"meteor" : false, // Meteor.js + + // Legacy + "nomen" : false, // true: Prohibit dangling `_` in variables + "onevar" : false, // true: Allow only one `var` statement per function + "passfail" : false, // true: Stop on first error + "white" : false, // true: Check against strict whitespace and indentation rules + + // Custom globals, from http://docs.meteor.com, in the order they appear there + "globals" : { + "Meteor": false, + "DDP": false, + "Session": false, + "Accounts": false, + "Template": false, + "Match": false, + "check": false, + "Deps": false, + "EJSON": false, + "HTTP": false, + "Email": false, + "Assets": false, + "Handlebars": false, // https://github.com/meteor/meteor/wiki/Handlebars + + // Meteor internals + "DDPServer": false, + "global": false, + "Log": false, + "MongoInternals": false, + "process": false, + "WebApp": false, + "WebAppInternals": false, + + // Globals useful when creating Meteor packages + "Package": false, + "Npm": false, + "Tinytest": false, + + // Common Meteor packages + "_": false, // Underscore.js + "$": false, // jQuery + "Router": false, // iron-router + "headers": false, // iron-router headers + "jQuery": false, + "SRP": false, + + // Packages + "Fiber": true, + "moment": true, + "Docker": true, + "byline": true, + "fs": true, + "zlib": true, + "tar": true, + "https": true, + "path": true, + "exec": true, + "gui": true, + "win": true, + "ga": true, + "chokidar": true, + "docker": true, + "async": true, + + // Collections + "SimpleSchema": false, + "ServiceConfiguration": false, + "Apps": true, + "schemaApps": true, + "Images": true, + "schemaImages": true, + "Installs": true, + "schemaInstalls": true, + + // Controllers + "RouteController": true, + "DashboardController": true, + "AppController": true, + "ImageController": true, + "SetupController": true, + + // Server and Client + "boot2dockerexec": true, + "getBinDir": true, + "getBoot2DockerIp": true, + "getBoot2DockerState": true, + "getBoot2DockerDiskUsage": true, + "getBoot2DockerMemoryUsage": true, + "getBoot2DockerInfo": true, + "boot2DockerVMExists": true, + "eraseBoot2DockerVMFiles": true, + "getHomePath": true, + "initBoot2Docker": true, + "isVirtualBoxInstalled": true, + "upgradeBoot2Docker": true, + "installBoot2DockerAddons": true, + "startBoot2Docker": true, + "stopBoot2Docker": true, + "checkBoot2DockerVM": true, + "resolveBoot2DockerVM": true, + "startFixInterval": true, + "trackLink": true, + "isResolverSetup": true, + "setupVirtualBoxAndResolver": true, + "setupVirtualBoxSharedFolder": true, + "updateBoot2DockerInfo": true, + "fixBoot2DockerVM": true, + "fixDefaultImages": true, + "fixDefaultContainers": true, + "fixInterval": true, + "stopFixInterval": true, + "runSetup": true, + "removeBindFolder": true, + "removeAppWatcher": true, + "addAppWatcher": true, + "resolveWatchers": true, + "recoverApps": true, + "restartApp": true, + "deleteApp": true, + "deleteFolder": true, + "loadKiteVolumes": true, + "getAppLogs": true, + "hasDockerfile": true, + "runContainer": true, + "runContainerSync": true, + "restartContainer": true, + "restartContainerSync": true, + "createTarFile": true, + "createTarFileSync": true, + "getImageData": true, + "getImageDataSync": true, + "removeImage": true, + "removeImageSync": true, + "deleteImage": true, + "checkDefaultImages": true, + "resolveDefaultImages": true, + "checkDefaultContainers": true, + "resolveDefaultContainers": true, + "killAndRemoveContainers": true, + "deleteImageSync": true, + "upContainers": true, + "reloadDefaultContainers": true, + "removeImages": true, + "pullImageFromDockerfile": true, + "buildImage": true, + "getImageMetaData": true, + "getImageJSON": true, + "rebuildImage": true, + "saveImageFolderSync": true, + "rebuildImageSync": true, + "saveImageFolder": true, + "copyFolder": true, + + // Forms + "showFormErrors": true, + "clearFormErrors": true, + "formValidate": true, + "FormSchema": true, + "showFormSuccess": true, + "resetForm": true, + "removeContainer": true, + "removeContainerSync": true, + "deleteAppSync": true, + "getContainerData": true, + "getContainerDataSync": true, + + // Testing + "require": false, + "suite": false, + "test": false, + "emit": false, + + // Constants + "KITE_PATH": true, + "KITE_TAR_PATH": true, + "KITE_IMAGES_PATH": true, + "COMMON_WEB_PORTS": true + + } +} diff --git a/meteor/.meteor/.gitignore b/meteor/.meteor/.gitignore new file mode 100755 index 0000000000..4083037423 --- /dev/null +++ b/meteor/.meteor/.gitignore @@ -0,0 +1 @@ +local diff --git a/meteor/.meteor/.id b/meteor/.meteor/.id new file mode 100644 index 0000000000..f5f1375b9f --- /dev/null +++ b/meteor/.meteor/.id @@ -0,0 +1,7 @@ +# This file contains a token that is unique to your project. +# Check it into your repository along with the rest of this directory. +# It can be used for purposes such as: +# - ensuring you don't accidentally deploy one app on top of another +# - providing package authors with aggregated statistics + +1povtxs1b790efiwuimu diff --git a/meteor/.meteor/identifier b/meteor/.meteor/identifier new file mode 100644 index 0000000000..6e3f17ff73 --- /dev/null +++ b/meteor/.meteor/identifier @@ -0,0 +1 @@ +jfhlbr1tt2eu81ijpxce diff --git a/meteor/.meteor/packages b/meteor/.meteor/packages new file mode 100755 index 0000000000..0739b78bee --- /dev/null +++ b/meteor/.meteor/packages @@ -0,0 +1,20 @@ +# Meteor packages used by this project, one per line. +# +# 'meteor add' and 'meteor remove' will edit this file for you, +# but you can also edit it by hand. + +standard-app-packages +less +bootstrap3-less +npm +iron-router +headers +handlebar-helpers +collection2 +collection-hooks +moment +underscore-string-latest +collection-helpers +octicons +fast-render +iron-router-ga diff --git a/meteor/.meteor/release b/meteor/.meteor/release new file mode 100755 index 0000000000..ee94dd834b --- /dev/null +++ b/meteor/.meteor/release @@ -0,0 +1 @@ +0.8.3 diff --git a/meteor/.meteor/versions b/meteor/.meteor/versions new file mode 100644 index 0000000000..de5ff9f39c --- /dev/null +++ b/meteor/.meteor/versions @@ -0,0 +1,60 @@ +accounts-base@1.0.0 +application-configuration@1.0.0 +autoupdate@1.0.4 +binary-heap@1.0.0 +blaze-tools@1.0.0 +blaze@1.0.3 +bootstrap3-less@0.0.0 +callback-hook@1.0.0 +check@1.0.0 +collection-helpers@0.0.0 +collection-hooks@0.0.0 +collection2@0.0.0 +ctl-helper@1.0.2 +ctl@1.0.0 +deps@1.0.0 +ejson@1.0.0 +fast-render@0.0.0 +follower-livedata@1.0.0 +geojson-utils@1.0.0 +handlebar-helpers@0.0.0 +headers@0.0.0 +html-tools@1.0.0 +htmljs@1.0.0 +id-map@1.0.0 +inject-initial@0.0.0 +iron-core@0.2.0 +iron-dynamic-template@0.2.1 +iron-layout@0.2.0 +iron-router@0.8.2 +jquery@1.0.0 +json@1.0.0 +less@1.0.4 +livedata@1.0.5 +localstorage@1.0.0 +logging@1.0.2 +meteor@1.0.2 +minifiers@1.0.2 +minimongo@1.0.1 +moment@0.0.0 +mongo-livedata@1.0.3 +npm@0.0.0 +observe-sequence@1.0.1 +octicons@0.0.0 +ordered-dict@1.0.0 +random@1.0.0 +reactive-dict@1.0.0 +reload@1.0.0 +retry@1.0.0 +routepolicy@1.0.0 +service-configuration@1.0.0 +session@1.0.0 +simple-schema@0.0.0 +spacebars-compiler@1.0.1 +spacebars@1.0.0 +standard-app-packages@1.0.0 +templating@1.0.4 +ui@1.0.0 +underscore-string-latest@0.0.0 +underscore@1.0.0 +webapp@1.0.2 diff --git a/meteor/client/javascripts/jquery.serializeObject.js b/meteor/client/javascripts/jquery.serializeObject.js new file mode 100755 index 0000000000..f6285acbd3 --- /dev/null +++ b/meteor/client/javascripts/jquery.serializeObject.js @@ -0,0 +1,40 @@ +// +// Use internal $.serializeArray to get list of form elements which is +// consistent with $.serialize +// +// From version 2.0.0, $.serializeObject will stop converting [name] values +// to camelCase format. This is *consistent* with other serialize methods: +// +// - $.serialize +// - $.serializeArray +// +// If you require camel casing, you can either download version 1.0.4 or map +// them yourself. +// + +(function($){ + $.fn.serializeObject = function () { + "use strict"; + + var result = {}; + var extend = function (i, element) { + var node = result[element.name]; + + // If node with same name exists already, need to convert it to an array as it + // is a multi-value field (i.e., checkboxes) + + if ('undefined' !== typeof node && node !== null) { + if ($.isArray(node)) { + node.push(element.value); + } else { + result[element.name] = [node, element.value]; + } + } else { + result[element.name] = element.value; + } + }; + + $.each(this.serializeArray(), extend); + return result; + }; +})(jQuery); diff --git a/meteor/client/lib/boot2docker.js b/meteor/client/lib/boot2docker.js new file mode 100644 index 0000000000..ccacff6ad4 --- /dev/null +++ b/meteor/client/lib/boot2docker.js @@ -0,0 +1,270 @@ +var exec = require('exec'); +var path = require('path'); + +boot2dockerexec = function (command, callback) { + exec(path.join(getBinDir(), 'boot2docker') + ' --lowerip=192.168.59.103 --upperip=192.168.59.103 --dhcp=false ' + command, function(err, stdout) { + callback(err, stdout); + }); +}; + +getBoot2DockerIp = function (callback) { + boot2dockerexec('ip', function (err, stdout) { + if (err) { + callback(err, null); + } else { + callback(null, stdout); + } + }); +}; + +getBoot2DockerState = function (callback) { + boot2dockerexec(' info', function (err, stdout) { + if (err) { + callback(err, null); + return; + } + try { + var info = JSON.parse(stdout); + callback(null, info.State); + } catch (e) { + callback(e, null); + } + }); +}; + +getBoot2DockerDiskUsage = function (callback) { + boot2dockerexec('ssh "df"', function (err, stdout) { + if (err) { + callback(err, null); + return; + } + try { + var lines = stdout.split('\n'); + var dataline = _.find(lines, function (line) { + return line.indexOf('/dev/sda1') !== -1; + }); + var tokens = dataline.split(' '); + tokens = tokens.filter(function (token) { + return token !== ''; + }); + var usedGb = parseInt(tokens[2], 10) / 1000000; + var totalGb = parseInt(tokens[3], 10) / 1000000; + var percent = parseInt(tokens[4].replace('%', ''), 10); + callback(null, { + used_gb: usedGb.toFixed(2), + total_gb: totalGb.toFixed(2), + percent: percent + }); + } catch (error) { + callback(err, null); + } + }); +}; + +getBoot2DockerMemoryUsage = function (callback) { + boot2dockerexec('ssh "free -m"', function (err, stdout) { + if (err) { + callback(err, null); + return; + } + try { + var lines = stdout.split('\n'); + var dataline = _.find(lines, function (line) { + return line.indexOf('-/+ buffers') !== -1; + }); + var tokens = dataline.split(' '); + tokens = tokens.filter(function(token) { + return token !== ''; + }); + var usedGb = parseInt(tokens[2], 10) / 1000; + var freeGb = parseInt(tokens[3], 10) / 1000; + var totalGb = usedGb + freeGb; + var percent = Math.round(usedGb / totalGb * 100); + callback(null, { + used_gb: usedGb.toFixed(2), + total_gb: totalGb.toFixed(2), + free_gb: freeGb.toFixed(2), + percent: percent + }); + } catch (error) { + callback(error, null); + } + }); +}; + +getBoot2DockerInfo = function (callback) { + getBoot2DockerState(function (err, state) { + if (err) { + callback(err, null); + return; + } + if (state === 'poweroff') { + callback(null, {state: state}); + } else { + getBoot2DockerMemoryUsage(function (err, mem) { + if (err) { callback(null, {state: state}); } + getBoot2DockerDiskUsage(function (err, disk) { + if (err) { callback(null, {state: state}); } + callback(null, { + state: state, + memory: mem, + disk: disk + }); + }); + }); + } + }); +}; + +boot2DockerVMExists = function (callback) { + boot2dockerexec('info', function (err) { + if (err) { + callback(null, false); + } else { + callback(null, true); + } + }); +}; + +eraseBoot2DockerVMFiles = function (callback) { + var VMFileLocation = path.join(getHomePath(), 'VirtualBox\\ VMs/boot2docker-vm'); + exec('rm -rf ' + VMFileLocation, function (err) { + callback(err); + }); +}; + +initBoot2Docker = function (callback) { + isVirtualBoxInstalled(function (err, installed) { + if (err) { + callback(err); + return; + } + if (installed) { + boot2dockerexec('init', function (err) { + console.log(err); + if (err) { + if (err.indexOf('exit status 1') !== -1) { + eraseBoot2DockerVMFiles(function () { + boot2dockerexec('init', function (err) { + callback(err); + }); + }); + } else { + callback(err); + } + } else { + callback(); + } + }); + } else { + callback(new Error('initBoot2Docker called but VirtualBox isn\'t installed.')); + } + }); +}; + +upgradeBoot2Docker = function (callback) { + boot2dockerexec('upgrade', function (err, stdout) { + console.log(stdout); + callback(err); + }); +}; + +installBoot2DockerAddons = function (callback) { + exec('/bin/cat ' + path.join(getBinDir(), 'kite-binaries.tar.gz') + ' | ' + path.join(getBinDir(), 'boot2docker') + ' ssh "tar zx -C /usr/local/bin"', function (err, stdout) { + console.log(stdout); + callback(err); + }); +}; + +startBoot2Docker = function (callback) { + isVirtualBoxInstalled(function (err, installed) { + if (err) { + callback(err); + return; + } + if (installed) { + boot2DockerVMExists(function (err, exists) { + if (exists) { + console.log('Running up (server)'); + boot2dockerexec('up', function (err, stdout) { + console.log(err); + console.log(stdout); + if (err) { + if (err.indexOf('Waiting for VM to be started') !== -1) { + installBoot2DockerAddons(function (err) { + callback(err); + }); + } else { + callback(err); + } + } else { + installBoot2DockerAddons(function (err) { + callback(err); + }); + } + }); + } else { + callback(new Error('startBoot2Docker called but boot2docker-vm doesn\'t exist.')); + } + }); + } else { + callback(new Error('startBoot2Docker called but VirtualBox isn\'t installed.')); + } + }); +}; + +stopBoot2Docker = function (callback) { + boot2dockerexec('stop', function (err, stdout) { + console.log(stdout); + console.log(err); + if (err) { + callback(err); + return; + } + callback(null); + }); +}; + +checkBoot2DockerVM = function (callback) { + boot2DockerVMExists(function (err) { + if (err) { + callback(err); + return; + } else { + getBoot2DockerState(function (err, state) { + if (state !== 'running') { + callback('boot2docker not running'); + } else { + callback(); + } + }); + } + }); +}; + +// Make sure the VM exists, is up and is running. +resolveBoot2DockerVM = function (callback) { + boot2DockerVMExists(function (err, exists) { + + // If somehow the boot2docker VM doesn't exist anymor then re-create it. + if (!exists) { + initBoot2Docker(function () { + startBoot2Docker(function (err) { + callback(err); + }); + }); + } else { + + // If it exists but it's not running.. restart it. + getBoot2DockerState(function (err, state) { + if (state !== 'running') { + startBoot2Docker(function (err) { + callback(err); + }); + } else { + callback(); + } + }); + } + }); +}; diff --git a/meteor/client/lib/router.js b/meteor/client/lib/router.js new file mode 100755 index 0000000000..0e9f2ecb6e --- /dev/null +++ b/meteor/client/lib/router.js @@ -0,0 +1,120 @@ +Router.configure({ + layoutTemplate: 'layout', + trackPageView: true +}); + +SetupController = RouteController.extend({ + layoutTemplate: 'setup_layout', + waitOn: function () { + return [Meteor.subscribe('installs')]; + } +}); + +DashboardController = RouteController.extend({ + layoutTemplate: 'dashboard_layout', + waitOn: function () { + return [Meteor.subscribe('apps'), Meteor.subscribe('images'), Meteor.subscribe('installs')]; + } +}); + +AppController = DashboardController.extend({ + layoutTemplate: 'dashboard_apps_layout', + data: function () { + return Apps.findOne({name: this.params.name}); + } +}); + +ImageController = DashboardController.extend({ + layoutTemplate: 'dashboard_images_layout', + data: function () { + return Images.findOne({_id: this.params.id}); + } +}); + +Router.map(function () { + + this.route('setup_intro', { + path: '/setup/intro', + controller: 'SetupController' + }); + + this.route('setup_install', { + path: '/setup/install', + controller: 'SetupController' + }); + + this.route('setup', { + path: '/', + controller: 'SetupController', + action: function () { + if (this.ready()) { + var install = Installs.findOne(); + if (!install) { + console.log('No installs detected, running installer again.'); + this.redirect('/setup/intro'); + } else { + startFixInterval(); + this.redirect('/apps'); + } + } + } + }); + + this.route('dashboard_apps', { + path: '/apps', + controller: 'DashboardController' + }); + + this.route('dashboard_apps_detail', { + path: '/apps/:name', + controller: 'AppController', + action: function () { + this.redirect('dashboard_apps_settings', {name: this.params.name}); + } + }); + + this.route('dashboard_images_detail', { + path: '/images/:id', + controller: 'ImageController', + action: function () { + this.redirect('dashboard_images_settings', {id: this.params.id}); + } + }); + + this.route('dashboard_images', { + path: '/images', + controller: 'DashboardController' + }); + + this.route('dashboard_images_logs', { + path: '/images/:id/logs', + controller: 'ImageController' + }); + + this.route('dashboard_images_settings', { + path: '/images/:id/settings', + controller: 'ImageController' + }); + + this.route('dashboard_apps_logs', { + path: '/apps/:name/logs', + controller: 'AppController' + }); + + this.route('dashboard_apps_settings', { + path: '/apps/:name/settings', + controller: 'AppController' + }); + + this.route('dashboard_settings', { + path: '/settings', + controller: 'DashboardController' + }); + + this.route('404', { + path: '*', + controller: 'DashboardController', + template: 'dashboard_apps' + }); + +}); diff --git a/meteor/client/lib/startup.js b/meteor/client/lib/startup.js new file mode 100644 index 0000000000..512c2ffc32 --- /dev/null +++ b/meteor/client/lib/startup.js @@ -0,0 +1,3 @@ +Meteor.startup(function () { + console.log('Kitematic started.'); +}); diff --git a/meteor/client/lib/utilities.js b/meteor/client/lib/utilities.js new file mode 100755 index 0000000000..90e8bda8b0 --- /dev/null +++ b/meteor/client/lib/utilities.js @@ -0,0 +1,44 @@ +var path = require('path'); + +getBinDir = function () { + if (process.env.NODE_ENV === 'development') { + return path.join(path.join(process.env.PWD, '..'), 'resources'); + } else { + return path.join(path.join(process.cwd(), '../../..'), 'resources'); + } +}; + +getHomePath = function () { + return process.env[(process.platform === 'win32') ? 'USERPROFILE' : 'HOME']; +}; + +showFormErrors = function ($form, errors) { + for (var name in errors) { + if (errors.hasOwnProperty(name)) { + var firstErrorMessage = errors[name][Object.keys(errors[name])[0]]; + $form.find('[name="' + name + '"]').parents('.form-group').addClass('has-error'); + var message = '

' + firstErrorMessage + '

'; + $form.find('[name="' + name + '"]').after(message); + } + } +}; + +showFormSuccess = function ($form) { + $form.find('input').parents('.form-group').addClass('has-success'); +}; + +clearFormErrors = function ($form) { + $form.find('.form-group.has-error .help-block.error').remove(); + $form.find('.form-group.has-error').removeClass('has-error'); +}; + +resetForm = function ($form) { + $form.find('input').val(''); +}; + +trackLink = function (trackLabel) { + if (trackLabel) { + console.log(trackLabel); + ga('send', 'event', 'link', 'click', trackLabel); + } +}; \ No newline at end of file diff --git a/meteor/client/lib/virtualbox.js b/meteor/client/lib/virtualbox.js new file mode 100644 index 0000000000..4000289978 --- /dev/null +++ b/meteor/client/lib/virtualbox.js @@ -0,0 +1,49 @@ +var fs = require('fs'); +var exec = require('exec'); +var path = require('path'); + +isVirtualBoxInstalled = function (callback) { + fs.exists('/usr/bin/VBoxManage', function (exists) { + callback(null, exists); + }); +}; + +isResolverSetup = function (callback) { + fs.readFile('/etc/resolver/dev', { + encoding: 'utf8' + }, function (err, data) { + if (err) { + callback(err, false); + } else { + if (data.indexOf('nameserver 172.17.42.1') !== -1) { + callback(null, true); + } else { + callback(null, false); + } + } + }); +}; + +setupVirtualBoxAndResolver = function (skipVirtualBox, callback) { + var installFile = path.join(getBinDir(), 'install'); + var cocoaSudo = path.join(getBinDir(), 'cocoasudo'); + var execCommand = cocoaSudo + ' --prompt="Kitematic Setup wants to make changes. Type your password to allow this." ' + installFile; + console.log(execCommand); + var env = { + VIRTUALBOX_PKG_PATH: path.join(getBinDir(), 'virtualbox-4.3.12.pkg') + }; + if (!skipVirtualBox) { + env.INSTALL_VIRTUALBOX = true; + } + exec(execCommand, {env: env}, function (err, stdout) { + console.log(stdout); + if (err) { + console.log(err); + callback(err); + return; + } + console.log('Virtualbox Installation & Resolver config complete.'); + callback(); + }); +}; + diff --git a/meteor/client/main.html b/meteor/client/main.html new file mode 100755 index 0000000000..248f9a3298 --- /dev/null +++ b/meteor/client/main.html @@ -0,0 +1,4 @@ + + Kitematic + + diff --git a/meteor/client/main.js b/meteor/client/main.js new file mode 100755 index 0000000000..6e53b6d71c --- /dev/null +++ b/meteor/client/main.js @@ -0,0 +1,165 @@ +try { + moment = require('moment'); + gui = require('nw.gui'); + gui.App.clearCache(); + win = gui.Window.get(); + var nativeMenuBar = new gui.Menu({type: 'menubar'}); + nativeMenuBar.createMacBuiltin('Kitematic'); + win.menu = nativeMenuBar; +} catch (e) { + console.error(e); +} + +document.addEventListener('dragover', function (e) { + e.preventDefault(); + e.stopPropagation(); +}, false); + +document.addEventListener('drop', function (e) { + e.preventDefault(); + e.stopPropagation(); +}, false); + +Handlebars.registerHelper('arrayify', function (obj) { + var result = []; + if (obj) { + _.each(Object.keys(obj), function (key) { + result.push({name: key, value: obj[key]}); + }); + } + return result; +}); + +Handlebars.registerHelper('setTitle', function (title) { + if (title) { + document.title = title + ' | Kitematic'; + } else { + document.title = 'Kitematic'; + } +}); + +Handlebars.registerHelper('cleanUrl', function (url) { + var tokens = url.split('/'); + return tokens[2]; +}); + +Handlebars.registerHelper('hasItem', function (array) { + if (array && typeof array.fetch === 'function') { + return array.fetch().length > 0; + } else { + return array.length > 0; + } +}); + +Handlebars.registerHelper('currentYear', function () { + return moment().format('YYYY'); +}); + +Handlebars.registerHelper('formatDate', function () { + return moment().format('MM/DD/YYYY - h:mm:ssA'); +}); + +Handlebars.registerHelper('timeSince', function (date) { + return moment(date).fromNow(); +}); + +Meteor.call('getDockerHost', function (err, host) { + if (err) { throw err; } + Session.set('dockerHost', host); +}); + +updateBoot2DockerInfo = function () { + getBoot2DockerInfo(function (err, info) { + if (err) { + return; + } + Session.set('boot2dockerState', info.state); + if (info.state !== 'poweroff' && info.memory && info.disk) { + Session.set('boot2dockerMemoryUsage', info.memory); + Session.set('boot2dockerDiskUsage', info.disk); + } + }); +}; + +fixBoot2DockerVM = function (callback) { + checkBoot2DockerVM(function (err) { + if (err) { + Session.set('available', false); + resolveBoot2DockerVM(function (err) { + if (err) { + callback(err); + } else { + Session.set('available', true); + callback(); + } + }); + } else { + callback(); + } + }); +}; + +fixDefaultImages = function (callback) { + Meteor.call('checkDefaultImages', function (err) { + if (err) { + Session.set('available', false); + Meteor.call('resolveDefaultImages', function (err) { + if (err) { + callback(err); + } else { + Session.set('available', true); + callback(); + } + }); + } else { + Session.set('available', true); + callback(); + } + }); +}; + +fixDefaultContainers = function (callback) { + Meteor.call('checkDefaultContainers', function (err) { + if (err) { + Session.set('available', false); + Meteor.call('resolveDefaultContainers', function (err) { + if (err) { + callback(err); + } else { + Session.set('available', true); + callback(); + } + }); + } else { + Session.set('available', true); + callback(); + } + }); +}; + +Meteor.setInterval(function () { + updateBoot2DockerInfo(); +}, 5000); + +fixInterval = null; +startFixInterval = function () { + stopFixInterval(); + fixInterval = Meteor.setInterval(function () { + fixBoot2DockerVM(function (err) { + if (err) { console.log(err); return; } + Meteor.call('resolveWatchers'); + Meteor.call('recoverApps'); + fixDefaultImages(function (err) { + if (err) { console.log(err); return; } + fixDefaultContainers(function (err) { + if (err) { console.log(err); } + }); + }); + }); + }, 5000); +}; + +stopFixInterval = function () { + Meteor.clearInterval(fixInterval); + fixInterval = null; +}; diff --git a/meteor/client/main.less b/meteor/client/main.less new file mode 100755 index 0000000000..b0d775924d --- /dev/null +++ b/meteor/client/main.less @@ -0,0 +1,11 @@ +@import "/packages/bootstrap3-less/bootstrap.import.less"; +@import "stylesheets/font-awesome/font-awesome.import.less"; +@import "stylesheets/typicons/typicons.import.less"; +@import "stylesheets/variables.import.less"; +@import "stylesheets/mixins.import.less"; +@import "stylesheets/theme.import.less"; +@import "stylesheets/widgets.import.less"; +@import "stylesheets/mac.import.less"; +@import "stylesheets/dashboard.import.less"; +@import "stylesheets/setup.import.less"; +@import "stylesheets/spinner.import.less"; diff --git a/meteor/client/stylesheets/dashboard.import.less b/meteor/client/stylesheets/dashboard.import.less new file mode 100755 index 0000000000..708e78e759 --- /dev/null +++ b/meteor/client/stylesheets/dashboard.import.less @@ -0,0 +1,269 @@ +.dashboard { + min-width: @window-width; + .container { + width: @window-width; + } + + .line-item() { + height: 42px; + padding-top: 0.2em; + padding-left: 0.6em; + padding-right: 0.6em; + border-bottom: 1px solid @text-color-lightest; + &:last-child { + border-bottom: 0; + } + .options { + position: relative; + float: right; + top: -24px; + .btn-icon { + font-size: 20px; + margin-left: 0.4em; + } + } + &:hover { + background-color: lighten(@text-color-lightest, 5%);; + } + a.error { + color: @brand-negative; + &:hover { + color: @brand-negative; + } + } + a.name { + position: relative; + top: 10px; + font-size: 15px; + font-weight: 400; + color: @text-color; + &:hover { + color: lighten(@text-color, 10%); + text-decoration: underline; + } + } + small { + position: relative; + font-weight: 300; + top: 10px; + margin-left: 1em; + font-size: 12px; + color: @text-color-light; + a { + color: @text-color-light; + &:hover { + color: lighten(@text-color-light, 10%); + text-decoration: underline; + } + } + } + .status { + float: left; + font-size: 30px; + position: relative; + margin-right: 0.3em; + } + } + + .dashboard-row { + .make-row(); + .dashboard-menu { + position: relative; + height: @window-height; + .make-xs-column(1); + .logo { + position: absolute; + margin-left: -2px; + bottom: 14px; + width: 38px; + height: 38px; + } + .nav-stacked { + margin-top: 42px; + } + .nav-pills > li > a { + i { + position: relative; + left: -11px; + } + margin-top: -4px; + font-size: 30px; + font-weight: 600; + border-radius: 0px; + color: rgba(255,255,255,0.5); + &:hover, &:focus { + color: rgba(255,255,255,0.8); + background-color: transparent; + } + } + .nav-pills > li.active > a, + .nav-pills > li.active > a:hover, + .nav-pills > li.active > a:focus { + color: rgba(255,255,255,1); + background-color: transparent; + } + } + .dashboard-body { + .make-xs-column(11); + min-height: @window-height; + padding: 0; + background-color: white; + .empty-placeholder { + padding-top: 5em; + padding-bottom: 2em; + text-align: center; + .big-icon i { + color: @text-color-lightest; + font-size: 200px; + } + h4 { + color: @text-color-lighter; + margin-bottom: 0.6em; + } + } + .header { + padding: 1.6em; + height: @dashboard-header-height; + .icons { + margin-left: 0.6em; + font-size: 15px; + a { + margin-left: 0.2em; + } + } + .options { + position: absolute; + top: 50px; + right: 18px; + } + } + .content { + .padded { + padding: 1.6em; + padding-top: 0; + } + &.longer { + min-height: @dashboard-content-height + @dashboard-header-height; + max-height: @dashboard-content-height + @dashboard-header-height; + } + min-height: @dashboard-content-height; + max-height: @dashboard-content-height; + overflow-y: overlay; + } + .nav-tabs > li, + .nav-pills > li { + float: none; + display: inline-block; + *display: inline; + zoom: 1; + } + .nav-tabs, .nav-pills { + text-align: center; + } + } + .app { + .line-item(); + .status { + top: 1px; + &.starting { + top: 3px; + font-size: 26px; + } + } + } + .image { + .line-item(); + .status { + top: 5px; + &.building { + font-size: 26px; + } + } + .avatar { + float: left; + font-size: 26px; + position: relative; + top: 5px; + width: 26px; + height: 26px; + border-radius: 26px; + margin-right: 0.3em; + } + } + } + + .logs { + background-color: @logs-background-color; + color: white; + padding: 1em; + font-family: monospace; + min-height: @dashboard-content-height; + } + + .warning-badge { + color: white; + width: 20px; + height: 20px; + position: absolute; + top: 5px; + right: -3px; + border-radius: 21px; + background-color: @brand-negative; + i { + position: relative; + left: 7px; + } + } + + .env-var-pair { + .make-row(); + margin-bottom: 0.2em; + font-size: 12px; + .env-var-key { + .make-xs-column(5); + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; + } + .env-var-value { + .make-xs-column(5); + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; + } + .options { + .make-xs-column(2); + } + } + + .utilization { + font-size: 12px; + } + + .disk-utilization { + margin-top: 2em; + } + + .progress { + font-weight: 600; + height: 24px; + border-radius: @border-radius-base; + } + + .progress-bar { + padding-top: 2px; + background-color: @brand-action; + border-radius: @border-radius-base; + } + + .download-status { + .progress { + border-radius: 0px; + margin-bottom: 0px; + } + .progress-bar { + background-color: darken(@logs-background-color, 1%); + border-radius: 0px; + } + } +} diff --git a/meteor/client/stylesheets/font-awesome/bordered-pulled.import.less b/meteor/client/stylesheets/font-awesome/bordered-pulled.import.less new file mode 100755 index 0000000000..0c90eb5672 --- /dev/null +++ b/meteor/client/stylesheets/font-awesome/bordered-pulled.import.less @@ -0,0 +1,16 @@ +// Bordered & Pulled +// ------------------------- + +.@{fa-css-prefix}-border { + padding: .2em .25em .15em; + border: solid .08em @fa-border-color; + border-radius: .1em; +} + +.pull-right { float: right; } +.pull-left { float: left; } + +.@{fa-css-prefix} { + &.pull-left { margin-right: .3em; } + &.pull-right { margin-left: .3em; } +} diff --git a/meteor/client/stylesheets/font-awesome/core.import.less b/meteor/client/stylesheets/font-awesome/core.import.less new file mode 100755 index 0000000000..6d223bc2f0 --- /dev/null +++ b/meteor/client/stylesheets/font-awesome/core.import.less @@ -0,0 +1,12 @@ +// Base Class Definition +// ------------------------- + +.@{fa-css-prefix} { + display: inline-block; + font-family: FontAwesome; + font-style: normal; + font-weight: normal; + line-height: 1; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} diff --git a/meteor/client/stylesheets/font-awesome/fixed-width.import.less b/meteor/client/stylesheets/font-awesome/fixed-width.import.less new file mode 100755 index 0000000000..110289f2f4 --- /dev/null +++ b/meteor/client/stylesheets/font-awesome/fixed-width.import.less @@ -0,0 +1,6 @@ +// Fixed Width Icons +// ------------------------- +.@{fa-css-prefix}-fw { + width: (18em / 14); + text-align: center; +} diff --git a/meteor/client/stylesheets/font-awesome/font-awesome.import.less b/meteor/client/stylesheets/font-awesome/font-awesome.import.less new file mode 100755 index 0000000000..24b3e211a8 --- /dev/null +++ b/meteor/client/stylesheets/font-awesome/font-awesome.import.less @@ -0,0 +1,17 @@ +/*! + * Font Awesome 4.1.0 by @davegandy - http://fontawesome.io - @fontawesome + * License - http://fontawesome.io/license (Font: SIL OFL 1.1, CSS: MIT License) + */ + +@import "variables.import.less"; +@import "mixins.import.less"; +@import "path.import.less"; +@import "core.import.less"; +@import "larger.import.less"; +@import "fixed-width.import.less"; +@import "list.import.less"; +@import "bordered-pulled.import.less"; +@import "spinning.import.less"; +@import "rotated-flipped.import.less"; +@import "stacked.import.less"; +@import "icons.import.less"; diff --git a/meteor/client/stylesheets/font-awesome/icons.import.less b/meteor/client/stylesheets/font-awesome/icons.import.less new file mode 100755 index 0000000000..13d8c685b0 --- /dev/null +++ b/meteor/client/stylesheets/font-awesome/icons.import.less @@ -0,0 +1,506 @@ +/* Font Awesome uses the Unicode Private Use Area (PUA) to ensure screen + readers do not read off random characters that represent icons */ + +.@{fa-css-prefix}-glass:before { content: @fa-var-glass; } +.@{fa-css-prefix}-music:before { content: @fa-var-music; } +.@{fa-css-prefix}-search:before { content: @fa-var-search; } +.@{fa-css-prefix}-envelope-o:before { content: @fa-var-envelope-o; } +.@{fa-css-prefix}-heart:before { content: @fa-var-heart; } +.@{fa-css-prefix}-star:before { content: @fa-var-star; } +.@{fa-css-prefix}-star-o:before { content: @fa-var-star-o; } +.@{fa-css-prefix}-user:before { content: @fa-var-user; } +.@{fa-css-prefix}-film:before { content: @fa-var-film; } +.@{fa-css-prefix}-th-large:before { content: @fa-var-th-large; } +.@{fa-css-prefix}-th:before { content: @fa-var-th; } +.@{fa-css-prefix}-th-list:before { content: @fa-var-th-list; } +.@{fa-css-prefix}-check:before { content: @fa-var-check; } +.@{fa-css-prefix}-times:before { content: @fa-var-times; } +.@{fa-css-prefix}-search-plus:before { content: @fa-var-search-plus; } +.@{fa-css-prefix}-search-minus:before { content: @fa-var-search-minus; } +.@{fa-css-prefix}-power-off:before { content: @fa-var-power-off; } +.@{fa-css-prefix}-signal:before { content: @fa-var-signal; } +.@{fa-css-prefix}-gear:before, +.@{fa-css-prefix}-cog:before { content: @fa-var-cog; } +.@{fa-css-prefix}-trash-o:before { content: @fa-var-trash-o; } +.@{fa-css-prefix}-home:before { content: @fa-var-home; } +.@{fa-css-prefix}-file-o:before { content: @fa-var-file-o; } +.@{fa-css-prefix}-clock-o:before { content: @fa-var-clock-o; } +.@{fa-css-prefix}-road:before { content: @fa-var-road; } +.@{fa-css-prefix}-download:before { content: @fa-var-download; } +.@{fa-css-prefix}-arrow-circle-o-down:before { content: @fa-var-arrow-circle-o-down; } +.@{fa-css-prefix}-arrow-circle-o-up:before { content: @fa-var-arrow-circle-o-up; } +.@{fa-css-prefix}-inbox:before { content: @fa-var-inbox; } +.@{fa-css-prefix}-play-circle-o:before { content: @fa-var-play-circle-o; } +.@{fa-css-prefix}-rotate-right:before, +.@{fa-css-prefix}-repeat:before { content: @fa-var-repeat; } +.@{fa-css-prefix}-refresh:before { content: @fa-var-refresh; } +.@{fa-css-prefix}-list-alt:before { content: @fa-var-list-alt; } +.@{fa-css-prefix}-lock:before { content: @fa-var-lock; } +.@{fa-css-prefix}-flag:before { content: @fa-var-flag; } +.@{fa-css-prefix}-headphones:before { content: @fa-var-headphones; } +.@{fa-css-prefix}-volume-off:before { content: @fa-var-volume-off; } +.@{fa-css-prefix}-volume-down:before { content: @fa-var-volume-down; } +.@{fa-css-prefix}-volume-up:before { content: @fa-var-volume-up; } +.@{fa-css-prefix}-qrcode:before { content: @fa-var-qrcode; } +.@{fa-css-prefix}-barcode:before { content: @fa-var-barcode; } +.@{fa-css-prefix}-tag:before { content: @fa-var-tag; } +.@{fa-css-prefix}-tags:before { content: @fa-var-tags; } +.@{fa-css-prefix}-book:before { content: @fa-var-book; } +.@{fa-css-prefix}-bookmark:before { content: @fa-var-bookmark; } +.@{fa-css-prefix}-print:before { content: @fa-var-print; } +.@{fa-css-prefix}-camera:before { content: @fa-var-camera; } +.@{fa-css-prefix}-font:before { content: @fa-var-font; } +.@{fa-css-prefix}-bold:before { content: @fa-var-bold; } +.@{fa-css-prefix}-italic:before { content: @fa-var-italic; } +.@{fa-css-prefix}-text-height:before { content: @fa-var-text-height; } +.@{fa-css-prefix}-text-width:before { content: @fa-var-text-width; } +.@{fa-css-prefix}-align-left:before { content: @fa-var-align-left; } +.@{fa-css-prefix}-align-center:before { content: @fa-var-align-center; } +.@{fa-css-prefix}-align-right:before { content: @fa-var-align-right; } +.@{fa-css-prefix}-align-justify:before { content: @fa-var-align-justify; } +.@{fa-css-prefix}-list:before { content: @fa-var-list; } +.@{fa-css-prefix}-dedent:before, +.@{fa-css-prefix}-outdent:before { content: @fa-var-outdent; } +.@{fa-css-prefix}-indent:before { content: @fa-var-indent; } +.@{fa-css-prefix}-video-camera:before { content: @fa-var-video-camera; } +.@{fa-css-prefix}-photo:before, +.@{fa-css-prefix}-image:before, +.@{fa-css-prefix}-picture-o:before { content: @fa-var-picture-o; } +.@{fa-css-prefix}-pencil:before { content: @fa-var-pencil; } +.@{fa-css-prefix}-map-marker:before { content: @fa-var-map-marker; } +.@{fa-css-prefix}-adjust:before { content: @fa-var-adjust; } +.@{fa-css-prefix}-tint:before { content: @fa-var-tint; } +.@{fa-css-prefix}-edit:before, +.@{fa-css-prefix}-pencil-square-o:before { content: @fa-var-pencil-square-o; } +.@{fa-css-prefix}-share-square-o:before { content: @fa-var-share-square-o; } +.@{fa-css-prefix}-check-square-o:before { content: @fa-var-check-square-o; } +.@{fa-css-prefix}-arrows:before { content: @fa-var-arrows; } +.@{fa-css-prefix}-step-backward:before { content: @fa-var-step-backward; } +.@{fa-css-prefix}-fast-backward:before { content: @fa-var-fast-backward; } +.@{fa-css-prefix}-backward:before { content: @fa-var-backward; } +.@{fa-css-prefix}-play:before { content: @fa-var-play; } +.@{fa-css-prefix}-pause:before { content: @fa-var-pause; } +.@{fa-css-prefix}-stop:before { content: @fa-var-stop; } +.@{fa-css-prefix}-forward:before { content: @fa-var-forward; } +.@{fa-css-prefix}-fast-forward:before { content: @fa-var-fast-forward; } +.@{fa-css-prefix}-step-forward:before { content: @fa-var-step-forward; } +.@{fa-css-prefix}-eject:before { content: @fa-var-eject; } +.@{fa-css-prefix}-chevron-left:before { content: @fa-var-chevron-left; } +.@{fa-css-prefix}-chevron-right:before { content: @fa-var-chevron-right; } +.@{fa-css-prefix}-plus-circle:before { content: @fa-var-plus-circle; } +.@{fa-css-prefix}-minus-circle:before { content: @fa-var-minus-circle; } +.@{fa-css-prefix}-times-circle:before { content: @fa-var-times-circle; } +.@{fa-css-prefix}-check-circle:before { content: @fa-var-check-circle; } +.@{fa-css-prefix}-question-circle:before { content: @fa-var-question-circle; } +.@{fa-css-prefix}-info-circle:before { content: @fa-var-info-circle; } +.@{fa-css-prefix}-crosshairs:before { content: @fa-var-crosshairs; } +.@{fa-css-prefix}-times-circle-o:before { content: @fa-var-times-circle-o; } +.@{fa-css-prefix}-check-circle-o:before { content: @fa-var-check-circle-o; } +.@{fa-css-prefix}-ban:before { content: @fa-var-ban; } +.@{fa-css-prefix}-arrow-left:before { content: @fa-var-arrow-left; } +.@{fa-css-prefix}-arrow-right:before { content: @fa-var-arrow-right; } +.@{fa-css-prefix}-arrow-up:before { content: @fa-var-arrow-up; } +.@{fa-css-prefix}-arrow-down:before { content: @fa-var-arrow-down; } +.@{fa-css-prefix}-mail-forward:before, +.@{fa-css-prefix}-share:before { content: @fa-var-share; } +.@{fa-css-prefix}-expand:before { content: @fa-var-expand; } +.@{fa-css-prefix}-compress:before { content: @fa-var-compress; } +.@{fa-css-prefix}-plus:before { content: @fa-var-plus; } +.@{fa-css-prefix}-minus:before { content: @fa-var-minus; } +.@{fa-css-prefix}-asterisk:before { content: @fa-var-asterisk; } +.@{fa-css-prefix}-exclamation-circle:before { content: @fa-var-exclamation-circle; } +.@{fa-css-prefix}-gift:before { content: @fa-var-gift; } +.@{fa-css-prefix}-leaf:before { content: @fa-var-leaf; } +.@{fa-css-prefix}-fire:before { content: @fa-var-fire; } +.@{fa-css-prefix}-eye:before { content: @fa-var-eye; } +.@{fa-css-prefix}-eye-slash:before { content: @fa-var-eye-slash; } +.@{fa-css-prefix}-warning:before, +.@{fa-css-prefix}-exclamation-triangle:before { content: @fa-var-exclamation-triangle; } +.@{fa-css-prefix}-plane:before { content: @fa-var-plane; } +.@{fa-css-prefix}-calendar:before { content: @fa-var-calendar; } +.@{fa-css-prefix}-random:before { content: @fa-var-random; } +.@{fa-css-prefix}-comment:before { content: @fa-var-comment; } +.@{fa-css-prefix}-magnet:before { content: @fa-var-magnet; } +.@{fa-css-prefix}-chevron-up:before { content: @fa-var-chevron-up; } +.@{fa-css-prefix}-chevron-down:before { content: @fa-var-chevron-down; } +.@{fa-css-prefix}-retweet:before { content: @fa-var-retweet; } +.@{fa-css-prefix}-shopping-cart:before { content: @fa-var-shopping-cart; } +.@{fa-css-prefix}-folder:before { content: @fa-var-folder; } +.@{fa-css-prefix}-folder-open:before { content: @fa-var-folder-open; } +.@{fa-css-prefix}-arrows-v:before { content: @fa-var-arrows-v; } +.@{fa-css-prefix}-arrows-h:before { content: @fa-var-arrows-h; } +.@{fa-css-prefix}-bar-chart-o:before { content: @fa-var-bar-chart-o; } +.@{fa-css-prefix}-twitter-square:before { content: @fa-var-twitter-square; } +.@{fa-css-prefix}-facebook-square:before { content: @fa-var-facebook-square; } +.@{fa-css-prefix}-camera-retro:before { content: @fa-var-camera-retro; } +.@{fa-css-prefix}-key:before { content: @fa-var-key; } +.@{fa-css-prefix}-gears:before, +.@{fa-css-prefix}-cogs:before { content: @fa-var-cogs; } +.@{fa-css-prefix}-comments:before { content: @fa-var-comments; } +.@{fa-css-prefix}-thumbs-o-up:before { content: @fa-var-thumbs-o-up; } +.@{fa-css-prefix}-thumbs-o-down:before { content: @fa-var-thumbs-o-down; } +.@{fa-css-prefix}-star-half:before { content: @fa-var-star-half; } +.@{fa-css-prefix}-heart-o:before { content: @fa-var-heart-o; } +.@{fa-css-prefix}-sign-out:before { content: @fa-var-sign-out; } +.@{fa-css-prefix}-linkedin-square:before { content: @fa-var-linkedin-square; } +.@{fa-css-prefix}-thumb-tack:before { content: @fa-var-thumb-tack; } +.@{fa-css-prefix}-external-link:before { content: @fa-var-external-link; } +.@{fa-css-prefix}-sign-in:before { content: @fa-var-sign-in; } +.@{fa-css-prefix}-trophy:before { content: @fa-var-trophy; } +.@{fa-css-prefix}-github-square:before { content: @fa-var-github-square; } +.@{fa-css-prefix}-upload:before { content: @fa-var-upload; } +.@{fa-css-prefix}-lemon-o:before { content: @fa-var-lemon-o; } +.@{fa-css-prefix}-phone:before { content: @fa-var-phone; } +.@{fa-css-prefix}-square-o:before { content: @fa-var-square-o; } +.@{fa-css-prefix}-bookmark-o:before { content: @fa-var-bookmark-o; } +.@{fa-css-prefix}-phone-square:before { content: @fa-var-phone-square; } +.@{fa-css-prefix}-twitter:before { content: @fa-var-twitter; } +.@{fa-css-prefix}-facebook:before { content: @fa-var-facebook; } +.@{fa-css-prefix}-github:before { content: @fa-var-github; } +.@{fa-css-prefix}-unlock:before { content: @fa-var-unlock; } +.@{fa-css-prefix}-credit-card:before { content: @fa-var-credit-card; } +.@{fa-css-prefix}-rss:before { content: @fa-var-rss; } +.@{fa-css-prefix}-hdd-o:before { content: @fa-var-hdd-o; } +.@{fa-css-prefix}-bullhorn:before { content: @fa-var-bullhorn; } +.@{fa-css-prefix}-bell:before { content: @fa-var-bell; } +.@{fa-css-prefix}-certificate:before { content: @fa-var-certificate; } +.@{fa-css-prefix}-hand-o-right:before { content: @fa-var-hand-o-right; } +.@{fa-css-prefix}-hand-o-left:before { content: @fa-var-hand-o-left; } +.@{fa-css-prefix}-hand-o-up:before { content: @fa-var-hand-o-up; } +.@{fa-css-prefix}-hand-o-down:before { content: @fa-var-hand-o-down; } +.@{fa-css-prefix}-arrow-circle-left:before { content: @fa-var-arrow-circle-left; } +.@{fa-css-prefix}-arrow-circle-right:before { content: @fa-var-arrow-circle-right; } +.@{fa-css-prefix}-arrow-circle-up:before { content: @fa-var-arrow-circle-up; } +.@{fa-css-prefix}-arrow-circle-down:before { content: @fa-var-arrow-circle-down; } +.@{fa-css-prefix}-globe:before { content: @fa-var-globe; } +.@{fa-css-prefix}-wrench:before { content: @fa-var-wrench; } +.@{fa-css-prefix}-tasks:before { content: @fa-var-tasks; } +.@{fa-css-prefix}-filter:before { content: @fa-var-filter; } +.@{fa-css-prefix}-briefcase:before { content: @fa-var-briefcase; } +.@{fa-css-prefix}-arrows-alt:before { content: @fa-var-arrows-alt; } +.@{fa-css-prefix}-group:before, +.@{fa-css-prefix}-users:before { content: @fa-var-users; } +.@{fa-css-prefix}-chain:before, +.@{fa-css-prefix}-link:before { content: @fa-var-link; } +.@{fa-css-prefix}-cloud:before { content: @fa-var-cloud; } +.@{fa-css-prefix}-flask:before { content: @fa-var-flask; } +.@{fa-css-prefix}-cut:before, +.@{fa-css-prefix}-scissors:before { content: @fa-var-scissors; } +.@{fa-css-prefix}-copy:before, +.@{fa-css-prefix}-files-o:before { content: @fa-var-files-o; } +.@{fa-css-prefix}-paperclip:before { content: @fa-var-paperclip; } +.@{fa-css-prefix}-save:before, +.@{fa-css-prefix}-floppy-o:before { content: @fa-var-floppy-o; } +.@{fa-css-prefix}-square:before { content: @fa-var-square; } +.@{fa-css-prefix}-navicon:before, +.@{fa-css-prefix}-reorder:before, +.@{fa-css-prefix}-bars:before { content: @fa-var-bars; } +.@{fa-css-prefix}-list-ul:before { content: @fa-var-list-ul; } +.@{fa-css-prefix}-list-ol:before { content: @fa-var-list-ol; } +.@{fa-css-prefix}-strikethrough:before { content: @fa-var-strikethrough; } +.@{fa-css-prefix}-underline:before { content: @fa-var-underline; } +.@{fa-css-prefix}-table:before { content: @fa-var-table; } +.@{fa-css-prefix}-magic:before { content: @fa-var-magic; } +.@{fa-css-prefix}-truck:before { content: @fa-var-truck; } +.@{fa-css-prefix}-pinterest:before { content: @fa-var-pinterest; } +.@{fa-css-prefix}-pinterest-square:before { content: @fa-var-pinterest-square; } +.@{fa-css-prefix}-google-plus-square:before { content: @fa-var-google-plus-square; } +.@{fa-css-prefix}-google-plus:before { content: @fa-var-google-plus; } +.@{fa-css-prefix}-money:before { content: @fa-var-money; } +.@{fa-css-prefix}-caret-down:before { content: @fa-var-caret-down; } +.@{fa-css-prefix}-caret-up:before { content: @fa-var-caret-up; } +.@{fa-css-prefix}-caret-left:before { content: @fa-var-caret-left; } +.@{fa-css-prefix}-caret-right:before { content: @fa-var-caret-right; } +.@{fa-css-prefix}-columns:before { content: @fa-var-columns; } +.@{fa-css-prefix}-unsorted:before, +.@{fa-css-prefix}-sort:before { content: @fa-var-sort; } +.@{fa-css-prefix}-sort-down:before, +.@{fa-css-prefix}-sort-desc:before { content: @fa-var-sort-desc; } +.@{fa-css-prefix}-sort-up:before, +.@{fa-css-prefix}-sort-asc:before { content: @fa-var-sort-asc; } +.@{fa-css-prefix}-envelope:before { content: @fa-var-envelope; } +.@{fa-css-prefix}-linkedin:before { content: @fa-var-linkedin; } +.@{fa-css-prefix}-rotate-left:before, +.@{fa-css-prefix}-undo:before { content: @fa-var-undo; } +.@{fa-css-prefix}-legal:before, +.@{fa-css-prefix}-gavel:before { content: @fa-var-gavel; } +.@{fa-css-prefix}-dashboard:before, +.@{fa-css-prefix}-tachometer:before { content: @fa-var-tachometer; } +.@{fa-css-prefix}-comment-o:before { content: @fa-var-comment-o; } +.@{fa-css-prefix}-comments-o:before { content: @fa-var-comments-o; } +.@{fa-css-prefix}-flash:before, +.@{fa-css-prefix}-bolt:before { content: @fa-var-bolt; } +.@{fa-css-prefix}-sitemap:before { content: @fa-var-sitemap; } +.@{fa-css-prefix}-umbrella:before { content: @fa-var-umbrella; } +.@{fa-css-prefix}-paste:before, +.@{fa-css-prefix}-clipboard:before { content: @fa-var-clipboard; } +.@{fa-css-prefix}-lightbulb-o:before { content: @fa-var-lightbulb-o; } +.@{fa-css-prefix}-exchange:before { content: @fa-var-exchange; } +.@{fa-css-prefix}-cloud-download:before { content: @fa-var-cloud-download; } +.@{fa-css-prefix}-cloud-upload:before { content: @fa-var-cloud-upload; } +.@{fa-css-prefix}-user-md:before { content: @fa-var-user-md; } +.@{fa-css-prefix}-stethoscope:before { content: @fa-var-stethoscope; } +.@{fa-css-prefix}-suitcase:before { content: @fa-var-suitcase; } +.@{fa-css-prefix}-bell-o:before { content: @fa-var-bell-o; } +.@{fa-css-prefix}-coffee:before { content: @fa-var-coffee; } +.@{fa-css-prefix}-cutlery:before { content: @fa-var-cutlery; } +.@{fa-css-prefix}-file-text-o:before { content: @fa-var-file-text-o; } +.@{fa-css-prefix}-building-o:before { content: @fa-var-building-o; } +.@{fa-css-prefix}-hospital-o:before { content: @fa-var-hospital-o; } +.@{fa-css-prefix}-ambulance:before { content: @fa-var-ambulance; } +.@{fa-css-prefix}-medkit:before { content: @fa-var-medkit; } +.@{fa-css-prefix}-fighter-jet:before { content: @fa-var-fighter-jet; } +.@{fa-css-prefix}-beer:before { content: @fa-var-beer; } +.@{fa-css-prefix}-h-square:before { content: @fa-var-h-square; } +.@{fa-css-prefix}-plus-square:before { content: @fa-var-plus-square; } +.@{fa-css-prefix}-angle-double-left:before { content: @fa-var-angle-double-left; } +.@{fa-css-prefix}-angle-double-right:before { content: @fa-var-angle-double-right; } +.@{fa-css-prefix}-angle-double-up:before { content: @fa-var-angle-double-up; } +.@{fa-css-prefix}-angle-double-down:before { content: @fa-var-angle-double-down; } +.@{fa-css-prefix}-angle-left:before { content: @fa-var-angle-left; } +.@{fa-css-prefix}-angle-right:before { content: @fa-var-angle-right; } +.@{fa-css-prefix}-angle-up:before { content: @fa-var-angle-up; } +.@{fa-css-prefix}-angle-down:before { content: @fa-var-angle-down; } +.@{fa-css-prefix}-desktop:before { content: @fa-var-desktop; } +.@{fa-css-prefix}-laptop:before { content: @fa-var-laptop; } +.@{fa-css-prefix}-tablet:before { content: @fa-var-tablet; } +.@{fa-css-prefix}-mobile-phone:before, +.@{fa-css-prefix}-mobile:before { content: @fa-var-mobile; } +.@{fa-css-prefix}-circle-o:before { content: @fa-var-circle-o; } +.@{fa-css-prefix}-quote-left:before { content: @fa-var-quote-left; } +.@{fa-css-prefix}-quote-right:before { content: @fa-var-quote-right; } +.@{fa-css-prefix}-spinner:before { content: @fa-var-spinner; } +.@{fa-css-prefix}-circle:before { content: @fa-var-circle; } +.@{fa-css-prefix}-mail-reply:before, +.@{fa-css-prefix}-reply:before { content: @fa-var-reply; } +.@{fa-css-prefix}-github-alt:before { content: @fa-var-github-alt; } +.@{fa-css-prefix}-folder-o:before { content: @fa-var-folder-o; } +.@{fa-css-prefix}-folder-open-o:before { content: @fa-var-folder-open-o; } +.@{fa-css-prefix}-smile-o:before { content: @fa-var-smile-o; } +.@{fa-css-prefix}-frown-o:before { content: @fa-var-frown-o; } +.@{fa-css-prefix}-meh-o:before { content: @fa-var-meh-o; } +.@{fa-css-prefix}-gamepad:before { content: @fa-var-gamepad; } +.@{fa-css-prefix}-keyboard-o:before { content: @fa-var-keyboard-o; } +.@{fa-css-prefix}-flag-o:before { content: @fa-var-flag-o; } +.@{fa-css-prefix}-flag-checkered:before { content: @fa-var-flag-checkered; } +.@{fa-css-prefix}-terminal:before { content: @fa-var-terminal; } +.@{fa-css-prefix}-code:before { content: @fa-var-code; } +.@{fa-css-prefix}-mail-reply-all:before, +.@{fa-css-prefix}-reply-all:before { content: @fa-var-reply-all; } +.@{fa-css-prefix}-star-half-empty:before, +.@{fa-css-prefix}-star-half-full:before, +.@{fa-css-prefix}-star-half-o:before { content: @fa-var-star-half-o; } +.@{fa-css-prefix}-location-arrow:before { content: @fa-var-location-arrow; } +.@{fa-css-prefix}-crop:before { content: @fa-var-crop; } +.@{fa-css-prefix}-code-fork:before { content: @fa-var-code-fork; } +.@{fa-css-prefix}-unlink:before, +.@{fa-css-prefix}-chain-broken:before { content: @fa-var-chain-broken; } +.@{fa-css-prefix}-question:before { content: @fa-var-question; } +.@{fa-css-prefix}-info:before { content: @fa-var-info; } +.@{fa-css-prefix}-exclamation:before { content: @fa-var-exclamation; } +.@{fa-css-prefix}-superscript:before { content: @fa-var-superscript; } +.@{fa-css-prefix}-subscript:before { content: @fa-var-subscript; } +.@{fa-css-prefix}-eraser:before { content: @fa-var-eraser; } +.@{fa-css-prefix}-puzzle-piece:before { content: @fa-var-puzzle-piece; } +.@{fa-css-prefix}-microphone:before { content: @fa-var-microphone; } +.@{fa-css-prefix}-microphone-slash:before { content: @fa-var-microphone-slash; } +.@{fa-css-prefix}-shield:before { content: @fa-var-shield; } +.@{fa-css-prefix}-calendar-o:before { content: @fa-var-calendar-o; } +.@{fa-css-prefix}-fire-extinguisher:before { content: @fa-var-fire-extinguisher; } +.@{fa-css-prefix}-rocket:before { content: @fa-var-rocket; } +.@{fa-css-prefix}-maxcdn:before { content: @fa-var-maxcdn; } +.@{fa-css-prefix}-chevron-circle-left:before { content: @fa-var-chevron-circle-left; } +.@{fa-css-prefix}-chevron-circle-right:before { content: @fa-var-chevron-circle-right; } +.@{fa-css-prefix}-chevron-circle-up:before { content: @fa-var-chevron-circle-up; } +.@{fa-css-prefix}-chevron-circle-down:before { content: @fa-var-chevron-circle-down; } +.@{fa-css-prefix}-html5:before { content: @fa-var-html5; } +.@{fa-css-prefix}-css3:before { content: @fa-var-css3; } +.@{fa-css-prefix}-anchor:before { content: @fa-var-anchor; } +.@{fa-css-prefix}-unlock-alt:before { content: @fa-var-unlock-alt; } +.@{fa-css-prefix}-bullseye:before { content: @fa-var-bullseye; } +.@{fa-css-prefix}-ellipsis-h:before { content: @fa-var-ellipsis-h; } +.@{fa-css-prefix}-ellipsis-v:before { content: @fa-var-ellipsis-v; } +.@{fa-css-prefix}-rss-square:before { content: @fa-var-rss-square; } +.@{fa-css-prefix}-play-circle:before { content: @fa-var-play-circle; } +.@{fa-css-prefix}-ticket:before { content: @fa-var-ticket; } +.@{fa-css-prefix}-minus-square:before { content: @fa-var-minus-square; } +.@{fa-css-prefix}-minus-square-o:before { content: @fa-var-minus-square-o; } +.@{fa-css-prefix}-level-up:before { content: @fa-var-level-up; } +.@{fa-css-prefix}-level-down:before { content: @fa-var-level-down; } +.@{fa-css-prefix}-check-square:before { content: @fa-var-check-square; } +.@{fa-css-prefix}-pencil-square:before { content: @fa-var-pencil-square; } +.@{fa-css-prefix}-external-link-square:before { content: @fa-var-external-link-square; } +.@{fa-css-prefix}-share-square:before { content: @fa-var-share-square; } +.@{fa-css-prefix}-compass:before { content: @fa-var-compass; } +.@{fa-css-prefix}-toggle-down:before, +.@{fa-css-prefix}-caret-square-o-down:before { content: @fa-var-caret-square-o-down; } +.@{fa-css-prefix}-toggle-up:before, +.@{fa-css-prefix}-caret-square-o-up:before { content: @fa-var-caret-square-o-up; } +.@{fa-css-prefix}-toggle-right:before, +.@{fa-css-prefix}-caret-square-o-right:before { content: @fa-var-caret-square-o-right; } +.@{fa-css-prefix}-euro:before, +.@{fa-css-prefix}-eur:before { content: @fa-var-eur; } +.@{fa-css-prefix}-gbp:before { content: @fa-var-gbp; } +.@{fa-css-prefix}-dollar:before, +.@{fa-css-prefix}-usd:before { content: @fa-var-usd; } +.@{fa-css-prefix}-rupee:before, +.@{fa-css-prefix}-inr:before { content: @fa-var-inr; } +.@{fa-css-prefix}-cny:before, +.@{fa-css-prefix}-rmb:before, +.@{fa-css-prefix}-yen:before, +.@{fa-css-prefix}-jpy:before { content: @fa-var-jpy; } +.@{fa-css-prefix}-ruble:before, +.@{fa-css-prefix}-rouble:before, +.@{fa-css-prefix}-rub:before { content: @fa-var-rub; } +.@{fa-css-prefix}-won:before, +.@{fa-css-prefix}-krw:before { content: @fa-var-krw; } +.@{fa-css-prefix}-bitcoin:before, +.@{fa-css-prefix}-btc:before { content: @fa-var-btc; } +.@{fa-css-prefix}-file:before { content: @fa-var-file; } +.@{fa-css-prefix}-file-text:before { content: @fa-var-file-text; } +.@{fa-css-prefix}-sort-alpha-asc:before { content: @fa-var-sort-alpha-asc; } +.@{fa-css-prefix}-sort-alpha-desc:before { content: @fa-var-sort-alpha-desc; } +.@{fa-css-prefix}-sort-amount-asc:before { content: @fa-var-sort-amount-asc; } +.@{fa-css-prefix}-sort-amount-desc:before { content: @fa-var-sort-amount-desc; } +.@{fa-css-prefix}-sort-numeric-asc:before { content: @fa-var-sort-numeric-asc; } +.@{fa-css-prefix}-sort-numeric-desc:before { content: @fa-var-sort-numeric-desc; } +.@{fa-css-prefix}-thumbs-up:before { content: @fa-var-thumbs-up; } +.@{fa-css-prefix}-thumbs-down:before { content: @fa-var-thumbs-down; } +.@{fa-css-prefix}-youtube-square:before { content: @fa-var-youtube-square; } +.@{fa-css-prefix}-youtube:before { content: @fa-var-youtube; } +.@{fa-css-prefix}-xing:before { content: @fa-var-xing; } +.@{fa-css-prefix}-xing-square:before { content: @fa-var-xing-square; } +.@{fa-css-prefix}-youtube-play:before { content: @fa-var-youtube-play; } +.@{fa-css-prefix}-dropbox:before { content: @fa-var-dropbox; } +.@{fa-css-prefix}-stack-overflow:before { content: @fa-var-stack-overflow; } +.@{fa-css-prefix}-instagram:before { content: @fa-var-instagram; } +.@{fa-css-prefix}-flickr:before { content: @fa-var-flickr; } +.@{fa-css-prefix}-adn:before { content: @fa-var-adn; } +.@{fa-css-prefix}-bitbucket:before { content: @fa-var-bitbucket; } +.@{fa-css-prefix}-bitbucket-square:before { content: @fa-var-bitbucket-square; } +.@{fa-css-prefix}-tumblr:before { content: @fa-var-tumblr; } +.@{fa-css-prefix}-tumblr-square:before { content: @fa-var-tumblr-square; } +.@{fa-css-prefix}-long-arrow-down:before { content: @fa-var-long-arrow-down; } +.@{fa-css-prefix}-long-arrow-up:before { content: @fa-var-long-arrow-up; } +.@{fa-css-prefix}-long-arrow-left:before { content: @fa-var-long-arrow-left; } +.@{fa-css-prefix}-long-arrow-right:before { content: @fa-var-long-arrow-right; } +.@{fa-css-prefix}-apple:before { content: @fa-var-apple; } +.@{fa-css-prefix}-windows:before { content: @fa-var-windows; } +.@{fa-css-prefix}-android:before { content: @fa-var-android; } +.@{fa-css-prefix}-linux:before { content: @fa-var-linux; } +.@{fa-css-prefix}-dribbble:before { content: @fa-var-dribbble; } +.@{fa-css-prefix}-skype:before { content: @fa-var-skype; } +.@{fa-css-prefix}-foursquare:before { content: @fa-var-foursquare; } +.@{fa-css-prefix}-trello:before { content: @fa-var-trello; } +.@{fa-css-prefix}-female:before { content: @fa-var-female; } +.@{fa-css-prefix}-male:before { content: @fa-var-male; } +.@{fa-css-prefix}-gittip:before { content: @fa-var-gittip; } +.@{fa-css-prefix}-sun-o:before { content: @fa-var-sun-o; } +.@{fa-css-prefix}-moon-o:before { content: @fa-var-moon-o; } +.@{fa-css-prefix}-archive:before { content: @fa-var-archive; } +.@{fa-css-prefix}-bug:before { content: @fa-var-bug; } +.@{fa-css-prefix}-vk:before { content: @fa-var-vk; } +.@{fa-css-prefix}-weibo:before { content: @fa-var-weibo; } +.@{fa-css-prefix}-renren:before { content: @fa-var-renren; } +.@{fa-css-prefix}-pagelines:before { content: @fa-var-pagelines; } +.@{fa-css-prefix}-stack-exchange:before { content: @fa-var-stack-exchange; } +.@{fa-css-prefix}-arrow-circle-o-right:before { content: @fa-var-arrow-circle-o-right; } +.@{fa-css-prefix}-arrow-circle-o-left:before { content: @fa-var-arrow-circle-o-left; } +.@{fa-css-prefix}-toggle-left:before, +.@{fa-css-prefix}-caret-square-o-left:before { content: @fa-var-caret-square-o-left; } +.@{fa-css-prefix}-dot-circle-o:before { content: @fa-var-dot-circle-o; } +.@{fa-css-prefix}-wheelchair:before { content: @fa-var-wheelchair; } +.@{fa-css-prefix}-vimeo-square:before { content: @fa-var-vimeo-square; } +.@{fa-css-prefix}-turkish-lira:before, +.@{fa-css-prefix}-try:before { content: @fa-var-try; } +.@{fa-css-prefix}-plus-square-o:before { content: @fa-var-plus-square-o; } +.@{fa-css-prefix}-space-shuttle:before { content: @fa-var-space-shuttle; } +.@{fa-css-prefix}-slack:before { content: @fa-var-slack; } +.@{fa-css-prefix}-envelope-square:before { content: @fa-var-envelope-square; } +.@{fa-css-prefix}-wordpress:before { content: @fa-var-wordpress; } +.@{fa-css-prefix}-openid:before { content: @fa-var-openid; } +.@{fa-css-prefix}-institution:before, +.@{fa-css-prefix}-bank:before, +.@{fa-css-prefix}-university:before { content: @fa-var-university; } +.@{fa-css-prefix}-mortar-board:before, +.@{fa-css-prefix}-graduation-cap:before { content: @fa-var-graduation-cap; } +.@{fa-css-prefix}-yahoo:before { content: @fa-var-yahoo; } +.@{fa-css-prefix}-google:before { content: @fa-var-google; } +.@{fa-css-prefix}-reddit:before { content: @fa-var-reddit; } +.@{fa-css-prefix}-reddit-square:before { content: @fa-var-reddit-square; } +.@{fa-css-prefix}-stumbleupon-circle:before { content: @fa-var-stumbleupon-circle; } +.@{fa-css-prefix}-stumbleupon:before { content: @fa-var-stumbleupon; } +.@{fa-css-prefix}-delicious:before { content: @fa-var-delicious; } +.@{fa-css-prefix}-digg:before { content: @fa-var-digg; } +.@{fa-css-prefix}-pied-piper-square:before, +.@{fa-css-prefix}-pied-piper:before { content: @fa-var-pied-piper; } +.@{fa-css-prefix}-pied-piper-alt:before { content: @fa-var-pied-piper-alt; } +.@{fa-css-prefix}-drupal:before { content: @fa-var-drupal; } +.@{fa-css-prefix}-joomla:before { content: @fa-var-joomla; } +.@{fa-css-prefix}-language:before { content: @fa-var-language; } +.@{fa-css-prefix}-fax:before { content: @fa-var-fax; } +.@{fa-css-prefix}-building:before { content: @fa-var-building; } +.@{fa-css-prefix}-child:before { content: @fa-var-child; } +.@{fa-css-prefix}-paw:before { content: @fa-var-paw; } +.@{fa-css-prefix}-spoon:before { content: @fa-var-spoon; } +.@{fa-css-prefix}-cube:before { content: @fa-var-cube; } +.@{fa-css-prefix}-cubes:before { content: @fa-var-cubes; } +.@{fa-css-prefix}-behance:before { content: @fa-var-behance; } +.@{fa-css-prefix}-behance-square:before { content: @fa-var-behance-square; } +.@{fa-css-prefix}-steam:before { content: @fa-var-steam; } +.@{fa-css-prefix}-steam-square:before { content: @fa-var-steam-square; } +.@{fa-css-prefix}-recycle:before { content: @fa-var-recycle; } +.@{fa-css-prefix}-automobile:before, +.@{fa-css-prefix}-car:before { content: @fa-var-car; } +.@{fa-css-prefix}-cab:before, +.@{fa-css-prefix}-taxi:before { content: @fa-var-taxi; } +.@{fa-css-prefix}-tree:before { content: @fa-var-tree; } +.@{fa-css-prefix}-spotify:before { content: @fa-var-spotify; } +.@{fa-css-prefix}-deviantart:before { content: @fa-var-deviantart; } +.@{fa-css-prefix}-soundcloud:before { content: @fa-var-soundcloud; } +.@{fa-css-prefix}-database:before { content: @fa-var-database; } +.@{fa-css-prefix}-file-pdf-o:before { content: @fa-var-file-pdf-o; } +.@{fa-css-prefix}-file-word-o:before { content: @fa-var-file-word-o; } +.@{fa-css-prefix}-file-excel-o:before { content: @fa-var-file-excel-o; } +.@{fa-css-prefix}-file-powerpoint-o:before { content: @fa-var-file-powerpoint-o; } +.@{fa-css-prefix}-file-photo-o:before, +.@{fa-css-prefix}-file-picture-o:before, +.@{fa-css-prefix}-file-image-o:before { content: @fa-var-file-image-o; } +.@{fa-css-prefix}-file-zip-o:before, +.@{fa-css-prefix}-file-archive-o:before { content: @fa-var-file-archive-o; } +.@{fa-css-prefix}-file-sound-o:before, +.@{fa-css-prefix}-file-audio-o:before { content: @fa-var-file-audio-o; } +.@{fa-css-prefix}-file-movie-o:before, +.@{fa-css-prefix}-file-video-o:before { content: @fa-var-file-video-o; } +.@{fa-css-prefix}-file-code-o:before { content: @fa-var-file-code-o; } +.@{fa-css-prefix}-vine:before { content: @fa-var-vine; } +.@{fa-css-prefix}-codepen:before { content: @fa-var-codepen; } +.@{fa-css-prefix}-jsfiddle:before { content: @fa-var-jsfiddle; } +.@{fa-css-prefix}-life-bouy:before, +.@{fa-css-prefix}-life-saver:before, +.@{fa-css-prefix}-support:before, +.@{fa-css-prefix}-life-ring:before { content: @fa-var-life-ring; } +.@{fa-css-prefix}-circle-o-notch:before { content: @fa-var-circle-o-notch; } +.@{fa-css-prefix}-ra:before, +.@{fa-css-prefix}-rebel:before { content: @fa-var-rebel; } +.@{fa-css-prefix}-ge:before, +.@{fa-css-prefix}-empire:before { content: @fa-var-empire; } +.@{fa-css-prefix}-git-square:before { content: @fa-var-git-square; } +.@{fa-css-prefix}-git:before { content: @fa-var-git; } +.@{fa-css-prefix}-hacker-news:before { content: @fa-var-hacker-news; } +.@{fa-css-prefix}-tencent-weibo:before { content: @fa-var-tencent-weibo; } +.@{fa-css-prefix}-qq:before { content: @fa-var-qq; } +.@{fa-css-prefix}-wechat:before, +.@{fa-css-prefix}-weixin:before { content: @fa-var-weixin; } +.@{fa-css-prefix}-send:before, +.@{fa-css-prefix}-paper-plane:before { content: @fa-var-paper-plane; } +.@{fa-css-prefix}-send-o:before, +.@{fa-css-prefix}-paper-plane-o:before { content: @fa-var-paper-plane-o; } +.@{fa-css-prefix}-history:before { content: @fa-var-history; } +.@{fa-css-prefix}-circle-thin:before { content: @fa-var-circle-thin; } +.@{fa-css-prefix}-header:before { content: @fa-var-header; } +.@{fa-css-prefix}-paragraph:before { content: @fa-var-paragraph; } +.@{fa-css-prefix}-sliders:before { content: @fa-var-sliders; } +.@{fa-css-prefix}-share-alt:before { content: @fa-var-share-alt; } +.@{fa-css-prefix}-share-alt-square:before { content: @fa-var-share-alt-square; } +.@{fa-css-prefix}-bomb:before { content: @fa-var-bomb; } diff --git a/meteor/client/stylesheets/font-awesome/larger.import.less b/meteor/client/stylesheets/font-awesome/larger.import.less new file mode 100755 index 0000000000..c9d646770e --- /dev/null +++ b/meteor/client/stylesheets/font-awesome/larger.import.less @@ -0,0 +1,13 @@ +// Icon Sizes +// ------------------------- + +/* makes the font 33% larger relative to the icon container */ +.@{fa-css-prefix}-lg { + font-size: (4em / 3); + line-height: (3em / 4); + vertical-align: -15%; +} +.@{fa-css-prefix}-2x { font-size: 2em; } +.@{fa-css-prefix}-3x { font-size: 3em; } +.@{fa-css-prefix}-4x { font-size: 4em; } +.@{fa-css-prefix}-5x { font-size: 5em; } diff --git a/meteor/client/stylesheets/font-awesome/list.import.less b/meteor/client/stylesheets/font-awesome/list.import.less new file mode 100755 index 0000000000..eed9340515 --- /dev/null +++ b/meteor/client/stylesheets/font-awesome/list.import.less @@ -0,0 +1,19 @@ +// List Icons +// ------------------------- + +.@{fa-css-prefix}-ul { + padding-left: 0; + margin-left: @fa-li-width; + list-style-type: none; + > li { position: relative; } +} +.@{fa-css-prefix}-li { + position: absolute; + left: -@fa-li-width; + width: @fa-li-width; + top: (2em / 14); + text-align: center; + &.@{fa-css-prefix}-lg { + left: -@fa-li-width + (4em / 14); + } +} diff --git a/meteor/client/stylesheets/font-awesome/mixins.import.less b/meteor/client/stylesheets/font-awesome/mixins.import.less new file mode 100755 index 0000000000..19e5a6457b --- /dev/null +++ b/meteor/client/stylesheets/font-awesome/mixins.import.less @@ -0,0 +1,20 @@ +// Mixins +// -------------------------- + +.fa-icon-rotate(@degrees, @rotation) { + filter: progid:DXImageTransform.Microsoft.BasicImage(rotation=@rotation); + -webkit-transform: rotate(@degrees); + -moz-transform: rotate(@degrees); + -ms-transform: rotate(@degrees); + -o-transform: rotate(@degrees); + transform: rotate(@degrees); +} + +.fa-icon-flip(@horiz, @vert, @rotation) { + filter: progid:DXImageTransform.Microsoft.BasicImage(rotation=@rotation, mirror=1); + -webkit-transform: scale(@horiz, @vert); + -moz-transform: scale(@horiz, @vert); + -ms-transform: scale(@horiz, @vert); + -o-transform: scale(@horiz, @vert); + transform: scale(@horiz, @vert); +} diff --git a/meteor/client/stylesheets/font-awesome/path.import.less b/meteor/client/stylesheets/font-awesome/path.import.less new file mode 100755 index 0000000000..d73bff8b50 --- /dev/null +++ b/meteor/client/stylesheets/font-awesome/path.import.less @@ -0,0 +1,14 @@ +/* FONT PATH + * -------------------------- */ + +@font-face { + font-family: 'FontAwesome'; + src: ~"url('@{fa-font-path}/fontawesome-webfont.eot?v=@{fa-version}')"; + src: ~"url('@{fa-font-path}/fontawesome-webfont.eot?#iefix&v=@{fa-version}') format('embedded-opentype')", + ~"url('@{fa-font-path}/fontawesome-webfont.woff?v=@{fa-version}') format('woff')", + ~"url('@{fa-font-path}/fontawesome-webfont.ttf?v=@{fa-version}') format('truetype')", + ~"url('@{fa-font-path}/fontawesome-webfont.svg?v=@{fa-version}#fontawesomeregular') format('svg')"; +// src: url('@{fa-font-path}/FontAwesome.otf') format('opentype'); // used when developing fonts + font-weight: normal; + font-style: normal; +} diff --git a/meteor/client/stylesheets/font-awesome/rotated-flipped.import.less b/meteor/client/stylesheets/font-awesome/rotated-flipped.import.less new file mode 100755 index 0000000000..8fff3a6c41 --- /dev/null +++ b/meteor/client/stylesheets/font-awesome/rotated-flipped.import.less @@ -0,0 +1,9 @@ +// Rotated & Flipped Icons +// ------------------------- + +.@{fa-css-prefix}-rotate-90 { .fa-icon-rotate(90deg, 1); } +.@{fa-css-prefix}-rotate-180 { .fa-icon-rotate(180deg, 2); } +.@{fa-css-prefix}-rotate-270 { .fa-icon-rotate(270deg, 3); } + +.@{fa-css-prefix}-flip-horizontal { .fa-icon-flip(-1, 1, 0); } +.@{fa-css-prefix}-flip-vertical { .fa-icon-flip(1, -1, 2); } diff --git a/meteor/client/stylesheets/font-awesome/spinning.import.less b/meteor/client/stylesheets/font-awesome/spinning.import.less new file mode 100755 index 0000000000..06b71ecb4c --- /dev/null +++ b/meteor/client/stylesheets/font-awesome/spinning.import.less @@ -0,0 +1,32 @@ +// Spinning Icons +// -------------------------- + +.@{fa-css-prefix}-spin { + -webkit-animation: spin 2s infinite linear; + -moz-animation: spin 2s infinite linear; + -o-animation: spin 2s infinite linear; + animation: spin 2s infinite linear; +} + +@-moz-keyframes spin { + 0% { -moz-transform: rotate(0deg); } + 100% { -moz-transform: rotate(359deg); } +} +@-webkit-keyframes spin { + 0% { -webkit-transform: rotate(0deg); } + 100% { -webkit-transform: rotate(359deg); } +} +@-o-keyframes spin { + 0% { -o-transform: rotate(0deg); } + 100% { -o-transform: rotate(359deg); } +} +@keyframes spin { + 0% { + -webkit-transform: rotate(0deg); + transform: rotate(0deg); + } + 100% { + -webkit-transform: rotate(359deg); + transform: rotate(359deg); + } +} diff --git a/meteor/client/stylesheets/font-awesome/stacked.import.less b/meteor/client/stylesheets/font-awesome/stacked.import.less new file mode 100755 index 0000000000..fc53fb0e7a --- /dev/null +++ b/meteor/client/stylesheets/font-awesome/stacked.import.less @@ -0,0 +1,20 @@ +// Stacked Icons +// ------------------------- + +.@{fa-css-prefix}-stack { + position: relative; + display: inline-block; + width: 2em; + height: 2em; + line-height: 2em; + vertical-align: middle; +} +.@{fa-css-prefix}-stack-1x, .@{fa-css-prefix}-stack-2x { + position: absolute; + left: 0; + width: 100%; + text-align: center; +} +.@{fa-css-prefix}-stack-1x { line-height: inherit; } +.@{fa-css-prefix}-stack-2x { font-size: 2em; } +.@{fa-css-prefix}-inverse { color: @fa-inverse; } diff --git a/meteor/client/stylesheets/font-awesome/variables.import.less b/meteor/client/stylesheets/font-awesome/variables.import.less new file mode 100755 index 0000000000..2fa3e014b7 --- /dev/null +++ b/meteor/client/stylesheets/font-awesome/variables.import.less @@ -0,0 +1,513 @@ +// Variables +// -------------------------- + +@fa-font-path: "/fonts"; +@fa-css-prefix: fa; +@fa-version: "4.1.0"; +@fa-border-color: #eee; +@fa-inverse: #fff; +@fa-li-width: (30em / 14); + +@fa-var-adjust: "\f042"; +@fa-var-adn: "\f170"; +@fa-var-align-center: "\f037"; +@fa-var-align-justify: "\f039"; +@fa-var-align-left: "\f036"; +@fa-var-align-right: "\f038"; +@fa-var-ambulance: "\f0f9"; +@fa-var-anchor: "\f13d"; +@fa-var-android: "\f17b"; +@fa-var-angle-double-down: "\f103"; +@fa-var-angle-double-left: "\f100"; +@fa-var-angle-double-right: "\f101"; +@fa-var-angle-double-up: "\f102"; +@fa-var-angle-down: "\f107"; +@fa-var-angle-left: "\f104"; +@fa-var-angle-right: "\f105"; +@fa-var-angle-up: "\f106"; +@fa-var-apple: "\f179"; +@fa-var-archive: "\f187"; +@fa-var-arrow-circle-down: "\f0ab"; +@fa-var-arrow-circle-left: "\f0a8"; +@fa-var-arrow-circle-o-down: "\f01a"; +@fa-var-arrow-circle-o-left: "\f190"; +@fa-var-arrow-circle-o-right: "\f18e"; +@fa-var-arrow-circle-o-up: "\f01b"; +@fa-var-arrow-circle-right: "\f0a9"; +@fa-var-arrow-circle-up: "\f0aa"; +@fa-var-arrow-down: "\f063"; +@fa-var-arrow-left: "\f060"; +@fa-var-arrow-right: "\f061"; +@fa-var-arrow-up: "\f062"; +@fa-var-arrows: "\f047"; +@fa-var-arrows-alt: "\f0b2"; +@fa-var-arrows-h: "\f07e"; +@fa-var-arrows-v: "\f07d"; +@fa-var-asterisk: "\f069"; +@fa-var-automobile: "\f1b9"; +@fa-var-backward: "\f04a"; +@fa-var-ban: "\f05e"; +@fa-var-bank: "\f19c"; +@fa-var-bar-chart-o: "\f080"; +@fa-var-barcode: "\f02a"; +@fa-var-bars: "\f0c9"; +@fa-var-beer: "\f0fc"; +@fa-var-behance: "\f1b4"; +@fa-var-behance-square: "\f1b5"; +@fa-var-bell: "\f0f3"; +@fa-var-bell-o: "\f0a2"; +@fa-var-bitbucket: "\f171"; +@fa-var-bitbucket-square: "\f172"; +@fa-var-bitcoin: "\f15a"; +@fa-var-bold: "\f032"; +@fa-var-bolt: "\f0e7"; +@fa-var-bomb: "\f1e2"; +@fa-var-book: "\f02d"; +@fa-var-bookmark: "\f02e"; +@fa-var-bookmark-o: "\f097"; +@fa-var-briefcase: "\f0b1"; +@fa-var-btc: "\f15a"; +@fa-var-bug: "\f188"; +@fa-var-building: "\f1ad"; +@fa-var-building-o: "\f0f7"; +@fa-var-bullhorn: "\f0a1"; +@fa-var-bullseye: "\f140"; +@fa-var-cab: "\f1ba"; +@fa-var-calendar: "\f073"; +@fa-var-calendar-o: "\f133"; +@fa-var-camera: "\f030"; +@fa-var-camera-retro: "\f083"; +@fa-var-car: "\f1b9"; +@fa-var-caret-down: "\f0d7"; +@fa-var-caret-left: "\f0d9"; +@fa-var-caret-right: "\f0da"; +@fa-var-caret-square-o-down: "\f150"; +@fa-var-caret-square-o-left: "\f191"; +@fa-var-caret-square-o-right: "\f152"; +@fa-var-caret-square-o-up: "\f151"; +@fa-var-caret-up: "\f0d8"; +@fa-var-certificate: "\f0a3"; +@fa-var-chain: "\f0c1"; +@fa-var-chain-broken: "\f127"; +@fa-var-check: "\f00c"; +@fa-var-check-circle: "\f058"; +@fa-var-check-circle-o: "\f05d"; +@fa-var-check-square: "\f14a"; +@fa-var-check-square-o: "\f046"; +@fa-var-chevron-circle-down: "\f13a"; +@fa-var-chevron-circle-left: "\f137"; +@fa-var-chevron-circle-right: "\f138"; +@fa-var-chevron-circle-up: "\f139"; +@fa-var-chevron-down: "\f078"; +@fa-var-chevron-left: "\f053"; +@fa-var-chevron-right: "\f054"; +@fa-var-chevron-up: "\f077"; +@fa-var-child: "\f1ae"; +@fa-var-circle: "\f111"; +@fa-var-circle-o: "\f10c"; +@fa-var-circle-o-notch: "\f1ce"; +@fa-var-circle-thin: "\f1db"; +@fa-var-clipboard: "\f0ea"; +@fa-var-clock-o: "\f017"; +@fa-var-cloud: "\f0c2"; +@fa-var-cloud-download: "\f0ed"; +@fa-var-cloud-upload: "\f0ee"; +@fa-var-cny: "\f157"; +@fa-var-code: "\f121"; +@fa-var-code-fork: "\f126"; +@fa-var-codepen: "\f1cb"; +@fa-var-coffee: "\f0f4"; +@fa-var-cog: "\f013"; +@fa-var-cogs: "\f085"; +@fa-var-columns: "\f0db"; +@fa-var-comment: "\f075"; +@fa-var-comment-o: "\f0e5"; +@fa-var-comments: "\f086"; +@fa-var-comments-o: "\f0e6"; +@fa-var-compass: "\f14e"; +@fa-var-compress: "\f066"; +@fa-var-copy: "\f0c5"; +@fa-var-credit-card: "\f09d"; +@fa-var-crop: "\f125"; +@fa-var-crosshairs: "\f05b"; +@fa-var-css3: "\f13c"; +@fa-var-cube: "\f1b2"; +@fa-var-cubes: "\f1b3"; +@fa-var-cut: "\f0c4"; +@fa-var-cutlery: "\f0f5"; +@fa-var-dashboard: "\f0e4"; +@fa-var-database: "\f1c0"; +@fa-var-dedent: "\f03b"; +@fa-var-delicious: "\f1a5"; +@fa-var-desktop: "\f108"; +@fa-var-deviantart: "\f1bd"; +@fa-var-digg: "\f1a6"; +@fa-var-dollar: "\f155"; +@fa-var-dot-circle-o: "\f192"; +@fa-var-download: "\f019"; +@fa-var-dribbble: "\f17d"; +@fa-var-dropbox: "\f16b"; +@fa-var-drupal: "\f1a9"; +@fa-var-edit: "\f044"; +@fa-var-eject: "\f052"; +@fa-var-ellipsis-h: "\f141"; +@fa-var-ellipsis-v: "\f142"; +@fa-var-empire: "\f1d1"; +@fa-var-envelope: "\f0e0"; +@fa-var-envelope-o: "\f003"; +@fa-var-envelope-square: "\f199"; +@fa-var-eraser: "\f12d"; +@fa-var-eur: "\f153"; +@fa-var-euro: "\f153"; +@fa-var-exchange: "\f0ec"; +@fa-var-exclamation: "\f12a"; +@fa-var-exclamation-circle: "\f06a"; +@fa-var-exclamation-triangle: "\f071"; +@fa-var-expand: "\f065"; +@fa-var-external-link: "\f08e"; +@fa-var-external-link-square: "\f14c"; +@fa-var-eye: "\f06e"; +@fa-var-eye-slash: "\f070"; +@fa-var-facebook: "\f09a"; +@fa-var-facebook-square: "\f082"; +@fa-var-fast-backward: "\f049"; +@fa-var-fast-forward: "\f050"; +@fa-var-fax: "\f1ac"; +@fa-var-female: "\f182"; +@fa-var-fighter-jet: "\f0fb"; +@fa-var-file: "\f15b"; +@fa-var-file-archive-o: "\f1c6"; +@fa-var-file-audio-o: "\f1c7"; +@fa-var-file-code-o: "\f1c9"; +@fa-var-file-excel-o: "\f1c3"; +@fa-var-file-image-o: "\f1c5"; +@fa-var-file-movie-o: "\f1c8"; +@fa-var-file-o: "\f016"; +@fa-var-file-pdf-o: "\f1c1"; +@fa-var-file-photo-o: "\f1c5"; +@fa-var-file-picture-o: "\f1c5"; +@fa-var-file-powerpoint-o: "\f1c4"; +@fa-var-file-sound-o: "\f1c7"; +@fa-var-file-text: "\f15c"; +@fa-var-file-text-o: "\f0f6"; +@fa-var-file-video-o: "\f1c8"; +@fa-var-file-word-o: "\f1c2"; +@fa-var-file-zip-o: "\f1c6"; +@fa-var-files-o: "\f0c5"; +@fa-var-film: "\f008"; +@fa-var-filter: "\f0b0"; +@fa-var-fire: "\f06d"; +@fa-var-fire-extinguisher: "\f134"; +@fa-var-flag: "\f024"; +@fa-var-flag-checkered: "\f11e"; +@fa-var-flag-o: "\f11d"; +@fa-var-flash: "\f0e7"; +@fa-var-flask: "\f0c3"; +@fa-var-flickr: "\f16e"; +@fa-var-floppy-o: "\f0c7"; +@fa-var-folder: "\f07b"; +@fa-var-folder-o: "\f114"; +@fa-var-folder-open: "\f07c"; +@fa-var-folder-open-o: "\f115"; +@fa-var-font: "\f031"; +@fa-var-forward: "\f04e"; +@fa-var-foursquare: "\f180"; +@fa-var-frown-o: "\f119"; +@fa-var-gamepad: "\f11b"; +@fa-var-gavel: "\f0e3"; +@fa-var-gbp: "\f154"; +@fa-var-ge: "\f1d1"; +@fa-var-gear: "\f013"; +@fa-var-gears: "\f085"; +@fa-var-gift: "\f06b"; +@fa-var-git: "\f1d3"; +@fa-var-git-square: "\f1d2"; +@fa-var-github: "\f09b"; +@fa-var-github-alt: "\f113"; +@fa-var-github-square: "\f092"; +@fa-var-gittip: "\f184"; +@fa-var-glass: "\f000"; +@fa-var-globe: "\f0ac"; +@fa-var-google: "\f1a0"; +@fa-var-google-plus: "\f0d5"; +@fa-var-google-plus-square: "\f0d4"; +@fa-var-graduation-cap: "\f19d"; +@fa-var-group: "\f0c0"; +@fa-var-h-square: "\f0fd"; +@fa-var-hacker-news: "\f1d4"; +@fa-var-hand-o-down: "\f0a7"; +@fa-var-hand-o-left: "\f0a5"; +@fa-var-hand-o-right: "\f0a4"; +@fa-var-hand-o-up: "\f0a6"; +@fa-var-hdd-o: "\f0a0"; +@fa-var-header: "\f1dc"; +@fa-var-headphones: "\f025"; +@fa-var-heart: "\f004"; +@fa-var-heart-o: "\f08a"; +@fa-var-history: "\f1da"; +@fa-var-home: "\f015"; +@fa-var-hospital-o: "\f0f8"; +@fa-var-html5: "\f13b"; +@fa-var-image: "\f03e"; +@fa-var-inbox: "\f01c"; +@fa-var-indent: "\f03c"; +@fa-var-info: "\f129"; +@fa-var-info-circle: "\f05a"; +@fa-var-inr: "\f156"; +@fa-var-instagram: "\f16d"; +@fa-var-institution: "\f19c"; +@fa-var-italic: "\f033"; +@fa-var-joomla: "\f1aa"; +@fa-var-jpy: "\f157"; +@fa-var-jsfiddle: "\f1cc"; +@fa-var-key: "\f084"; +@fa-var-keyboard-o: "\f11c"; +@fa-var-krw: "\f159"; +@fa-var-language: "\f1ab"; +@fa-var-laptop: "\f109"; +@fa-var-leaf: "\f06c"; +@fa-var-legal: "\f0e3"; +@fa-var-lemon-o: "\f094"; +@fa-var-level-down: "\f149"; +@fa-var-level-up: "\f148"; +@fa-var-life-bouy: "\f1cd"; +@fa-var-life-ring: "\f1cd"; +@fa-var-life-saver: "\f1cd"; +@fa-var-lightbulb-o: "\f0eb"; +@fa-var-link: "\f0c1"; +@fa-var-linkedin: "\f0e1"; +@fa-var-linkedin-square: "\f08c"; +@fa-var-linux: "\f17c"; +@fa-var-list: "\f03a"; +@fa-var-list-alt: "\f022"; +@fa-var-list-ol: "\f0cb"; +@fa-var-list-ul: "\f0ca"; +@fa-var-location-arrow: "\f124"; +@fa-var-lock: "\f023"; +@fa-var-long-arrow-down: "\f175"; +@fa-var-long-arrow-left: "\f177"; +@fa-var-long-arrow-right: "\f178"; +@fa-var-long-arrow-up: "\f176"; +@fa-var-magic: "\f0d0"; +@fa-var-magnet: "\f076"; +@fa-var-mail-forward: "\f064"; +@fa-var-mail-reply: "\f112"; +@fa-var-mail-reply-all: "\f122"; +@fa-var-male: "\f183"; +@fa-var-map-marker: "\f041"; +@fa-var-maxcdn: "\f136"; +@fa-var-medkit: "\f0fa"; +@fa-var-meh-o: "\f11a"; +@fa-var-microphone: "\f130"; +@fa-var-microphone-slash: "\f131"; +@fa-var-minus: "\f068"; +@fa-var-minus-circle: "\f056"; +@fa-var-minus-square: "\f146"; +@fa-var-minus-square-o: "\f147"; +@fa-var-mobile: "\f10b"; +@fa-var-mobile-phone: "\f10b"; +@fa-var-money: "\f0d6"; +@fa-var-moon-o: "\f186"; +@fa-var-mortar-board: "\f19d"; +@fa-var-music: "\f001"; +@fa-var-navicon: "\f0c9"; +@fa-var-openid: "\f19b"; +@fa-var-outdent: "\f03b"; +@fa-var-pagelines: "\f18c"; +@fa-var-paper-plane: "\f1d8"; +@fa-var-paper-plane-o: "\f1d9"; +@fa-var-paperclip: "\f0c6"; +@fa-var-paragraph: "\f1dd"; +@fa-var-paste: "\f0ea"; +@fa-var-pause: "\f04c"; +@fa-var-paw: "\f1b0"; +@fa-var-pencil: "\f040"; +@fa-var-pencil-square: "\f14b"; +@fa-var-pencil-square-o: "\f044"; +@fa-var-phone: "\f095"; +@fa-var-phone-square: "\f098"; +@fa-var-photo: "\f03e"; +@fa-var-picture-o: "\f03e"; +@fa-var-pied-piper: "\f1a7"; +@fa-var-pied-piper-alt: "\f1a8"; +@fa-var-pied-piper-square: "\f1a7"; +@fa-var-pinterest: "\f0d2"; +@fa-var-pinterest-square: "\f0d3"; +@fa-var-plane: "\f072"; +@fa-var-play: "\f04b"; +@fa-var-play-circle: "\f144"; +@fa-var-play-circle-o: "\f01d"; +@fa-var-plus: "\f067"; +@fa-var-plus-circle: "\f055"; +@fa-var-plus-square: "\f0fe"; +@fa-var-plus-square-o: "\f196"; +@fa-var-power-off: "\f011"; +@fa-var-print: "\f02f"; +@fa-var-puzzle-piece: "\f12e"; +@fa-var-qq: "\f1d6"; +@fa-var-qrcode: "\f029"; +@fa-var-question: "\f128"; +@fa-var-question-circle: "\f059"; +@fa-var-quote-left: "\f10d"; +@fa-var-quote-right: "\f10e"; +@fa-var-ra: "\f1d0"; +@fa-var-random: "\f074"; +@fa-var-rebel: "\f1d0"; +@fa-var-recycle: "\f1b8"; +@fa-var-reddit: "\f1a1"; +@fa-var-reddit-square: "\f1a2"; +@fa-var-refresh: "\f021"; +@fa-var-renren: "\f18b"; +@fa-var-reorder: "\f0c9"; +@fa-var-repeat: "\f01e"; +@fa-var-reply: "\f112"; +@fa-var-reply-all: "\f122"; +@fa-var-retweet: "\f079"; +@fa-var-rmb: "\f157"; +@fa-var-road: "\f018"; +@fa-var-rocket: "\f135"; +@fa-var-rotate-left: "\f0e2"; +@fa-var-rotate-right: "\f01e"; +@fa-var-rouble: "\f158"; +@fa-var-rss: "\f09e"; +@fa-var-rss-square: "\f143"; +@fa-var-rub: "\f158"; +@fa-var-ruble: "\f158"; +@fa-var-rupee: "\f156"; +@fa-var-save: "\f0c7"; +@fa-var-scissors: "\f0c4"; +@fa-var-search: "\f002"; +@fa-var-search-minus: "\f010"; +@fa-var-search-plus: "\f00e"; +@fa-var-send: "\f1d8"; +@fa-var-send-o: "\f1d9"; +@fa-var-share: "\f064"; +@fa-var-share-alt: "\f1e0"; +@fa-var-share-alt-square: "\f1e1"; +@fa-var-share-square: "\f14d"; +@fa-var-share-square-o: "\f045"; +@fa-var-shield: "\f132"; +@fa-var-shopping-cart: "\f07a"; +@fa-var-sign-in: "\f090"; +@fa-var-sign-out: "\f08b"; +@fa-var-signal: "\f012"; +@fa-var-sitemap: "\f0e8"; +@fa-var-skype: "\f17e"; +@fa-var-slack: "\f198"; +@fa-var-sliders: "\f1de"; +@fa-var-smile-o: "\f118"; +@fa-var-sort: "\f0dc"; +@fa-var-sort-alpha-asc: "\f15d"; +@fa-var-sort-alpha-desc: "\f15e"; +@fa-var-sort-amount-asc: "\f160"; +@fa-var-sort-amount-desc: "\f161"; +@fa-var-sort-asc: "\f0de"; +@fa-var-sort-desc: "\f0dd"; +@fa-var-sort-down: "\f0dd"; +@fa-var-sort-numeric-asc: "\f162"; +@fa-var-sort-numeric-desc: "\f163"; +@fa-var-sort-up: "\f0de"; +@fa-var-soundcloud: "\f1be"; +@fa-var-space-shuttle: "\f197"; +@fa-var-spinner: "\f110"; +@fa-var-spoon: "\f1b1"; +@fa-var-spotify: "\f1bc"; +@fa-var-square: "\f0c8"; +@fa-var-square-o: "\f096"; +@fa-var-stack-exchange: "\f18d"; +@fa-var-stack-overflow: "\f16c"; +@fa-var-star: "\f005"; +@fa-var-star-half: "\f089"; +@fa-var-star-half-empty: "\f123"; +@fa-var-star-half-full: "\f123"; +@fa-var-star-half-o: "\f123"; +@fa-var-star-o: "\f006"; +@fa-var-steam: "\f1b6"; +@fa-var-steam-square: "\f1b7"; +@fa-var-step-backward: "\f048"; +@fa-var-step-forward: "\f051"; +@fa-var-stethoscope: "\f0f1"; +@fa-var-stop: "\f04d"; +@fa-var-strikethrough: "\f0cc"; +@fa-var-stumbleupon: "\f1a4"; +@fa-var-stumbleupon-circle: "\f1a3"; +@fa-var-subscript: "\f12c"; +@fa-var-suitcase: "\f0f2"; +@fa-var-sun-o: "\f185"; +@fa-var-superscript: "\f12b"; +@fa-var-support: "\f1cd"; +@fa-var-table: "\f0ce"; +@fa-var-tablet: "\f10a"; +@fa-var-tachometer: "\f0e4"; +@fa-var-tag: "\f02b"; +@fa-var-tags: "\f02c"; +@fa-var-tasks: "\f0ae"; +@fa-var-taxi: "\f1ba"; +@fa-var-tencent-weibo: "\f1d5"; +@fa-var-terminal: "\f120"; +@fa-var-text-height: "\f034"; +@fa-var-text-width: "\f035"; +@fa-var-th: "\f00a"; +@fa-var-th-large: "\f009"; +@fa-var-th-list: "\f00b"; +@fa-var-thumb-tack: "\f08d"; +@fa-var-thumbs-down: "\f165"; +@fa-var-thumbs-o-down: "\f088"; +@fa-var-thumbs-o-up: "\f087"; +@fa-var-thumbs-up: "\f164"; +@fa-var-ticket: "\f145"; +@fa-var-times: "\f00d"; +@fa-var-times-circle: "\f057"; +@fa-var-times-circle-o: "\f05c"; +@fa-var-tint: "\f043"; +@fa-var-toggle-down: "\f150"; +@fa-var-toggle-left: "\f191"; +@fa-var-toggle-right: "\f152"; +@fa-var-toggle-up: "\f151"; +@fa-var-trash-o: "\f014"; +@fa-var-tree: "\f1bb"; +@fa-var-trello: "\f181"; +@fa-var-trophy: "\f091"; +@fa-var-truck: "\f0d1"; +@fa-var-try: "\f195"; +@fa-var-tumblr: "\f173"; +@fa-var-tumblr-square: "\f174"; +@fa-var-turkish-lira: "\f195"; +@fa-var-twitter: "\f099"; +@fa-var-twitter-square: "\f081"; +@fa-var-umbrella: "\f0e9"; +@fa-var-underline: "\f0cd"; +@fa-var-undo: "\f0e2"; +@fa-var-university: "\f19c"; +@fa-var-unlink: "\f127"; +@fa-var-unlock: "\f09c"; +@fa-var-unlock-alt: "\f13e"; +@fa-var-unsorted: "\f0dc"; +@fa-var-upload: "\f093"; +@fa-var-usd: "\f155"; +@fa-var-user: "\f007"; +@fa-var-user-md: "\f0f0"; +@fa-var-users: "\f0c0"; +@fa-var-video-camera: "\f03d"; +@fa-var-vimeo-square: "\f194"; +@fa-var-vine: "\f1ca"; +@fa-var-vk: "\f189"; +@fa-var-volume-down: "\f027"; +@fa-var-volume-off: "\f026"; +@fa-var-volume-up: "\f028"; +@fa-var-warning: "\f071"; +@fa-var-wechat: "\f1d7"; +@fa-var-weibo: "\f18a"; +@fa-var-weixin: "\f1d7"; +@fa-var-wheelchair: "\f193"; +@fa-var-windows: "\f17a"; +@fa-var-won: "\f159"; +@fa-var-wordpress: "\f19a"; +@fa-var-wrench: "\f0ad"; +@fa-var-xing: "\f168"; +@fa-var-xing-square: "\f169"; +@fa-var-yahoo: "\f19e"; +@fa-var-yen: "\f157"; +@fa-var-youtube: "\f167"; +@fa-var-youtube-play: "\f16a"; +@fa-var-youtube-square: "\f166"; diff --git a/meteor/client/stylesheets/mac.import.less b/meteor/client/stylesheets/mac.import.less new file mode 100644 index 0000000000..ecda84c646 --- /dev/null +++ b/meteor/client/stylesheets/mac.import.less @@ -0,0 +1,55 @@ +.traffic-light() { + width: 12px; + height: 12px; + border-radius: 12px; + display: inline-block; + cursor: pointer; + font-size: 10px; + i { + position: relative; + top: -1px; + left: 2px; + } +} + +.mac-window-options { + position: absolute; + top: 7px; + left: 10px; + .mac-close { + .traffic-light(); + background-color: #FA5F56; + color: darken(#C95A57, 10%); + } + .mac-minimize { + .traffic-light(); + background-color: #FEBB27; + color: darken(#D9AB43, 10%); + } + .mac-maximize { + .traffic-light(); + background-color: #23C93E; + color: darken(#37A345, 10%); + } +} + +.mac-window-header { + text-align: center; + border-bottom: 1px solid @text-color-lightest; + padding-top: 6px; + padding-bottom: 6px; + -webkit-app-region: drag; + a { + outline: 0; + &:focus { + text-decoration: none; + } + cursor: default; + font-size: 13px; + color: @text-color-light; + font-weight: 500; + &:hover { + text-decoration: none; + } + } +} diff --git a/meteor/client/stylesheets/mixins.import.less b/meteor/client/stylesheets/mixins.import.less new file mode 100644 index 0000000000..6676691544 --- /dev/null +++ b/meteor/client/stylesheets/mixins.import.less @@ -0,0 +1,14 @@ +.mac-gradient(@start-color, @end-color) { + background-image: -o-linear-gradient(-89deg, @start-color 0%, @end-color 100%); + background-image: -moz-linear-gradient(-89deg, @start-color 0%, @end-color 100%); + background-image: -ms-linear-gradient(-89deg, @start-color 0%, @end-color 100%); + background-image: linear-gradient(-179deg, @start-color 0%, @end-color 100%); +} + +.no-select() { + -ms-user-select: none; + -moz-user-select: -moz-none; + -khtml-user-select: none; + -webkit-user-select: none; + user-select: none; +} diff --git a/meteor/client/stylesheets/setup.import.less b/meteor/client/stylesheets/setup.import.less new file mode 100644 index 0000000000..347e16c4a9 --- /dev/null +++ b/meteor/client/stylesheets/setup.import.less @@ -0,0 +1,51 @@ +.setup { + + -webkit-app-region: drag; + height: 100%; + background: #F8F8F8; + + .content { + margin-top: 30px; + } + + .steps { + margin-left: 180px; + margin-top: 60px; + .media-body { + margin-top: 5px; + font-size: 18px; + padding-left: 14px; + } + } + + h2 { + padding-top: 30px; + font-weight: 500; + font-size: 36px; + color: #3E3E3E; + } + + p { + margin-top: 20px; + font-size: 14px; + color: #232323; + font-weight: 500; + } + + .install_logo { + margin-top: 60px; + } + + .install-finish { + margin-top: 60px; + } + + .install-continue { + margin-top: 140px; + p { + margin-top: 0; + font-size: 13px; + } + } + +} diff --git a/meteor/client/stylesheets/spinner.import.less b/meteor/client/stylesheets/spinner.import.less new file mode 100644 index 0000000000..9c38f3b849 --- /dev/null +++ b/meteor/client/stylesheets/spinner.import.less @@ -0,0 +1,83 @@ +.ajax-spinner-bars { + position:relative; + width:34px; + height:34px; + display: inline-block; +} +.ajax-spinner-bars > div { + position: absolute; + width: 2px; + height: 8px; + left: 17px; + top: 13px; + background-color: #25363F; + -webkit-animation: fadeit 0.8s linear infinite; +} +.ajax-spinner-bars > .bar-1 { + -webkit-transform: rotate(0deg) translate(0, -12px); + -webkit-animation-delay:-0.8s; +} +.ajax-spinner-bars > .bar-2 { + -webkit-transform: rotate(22.5deg) translate(0, -12px); + -webkit-animation-delay:-0.75s; +} +.ajax-spinner-bars > .bar-3 { + -webkit-transform: rotate(45deg) translate(0, -12px); + -webkit-animation-delay:-0.7s; +} +.ajax-spinner-bars > .bar-4 { + -webkit-transform: rotate(67.5deg) translate(0, -12px); + -webkit-animation-delay:-0.65s; +} +.ajax-spinner-bars > .bar-5 { + -webkit-transform: rotate(90deg) translate(0, -12px); + -webkit-animation-delay:-0.6s; +} +.ajax-spinner-bars > .bar-6 { + -webkit-transform: rotate(112.5deg) translate(0, -12px); + -webkit-animation-delay:-0.55s; +} +.ajax-spinner-bars > .bar-7 { + -webkit-transform: rotate(135deg) translate(0, -12px); + -webkit-animation-delay:-0.5s; +} +.ajax-spinner-bars > .bar-8 { + -webkit-transform: rotate(157.5deg) translate(0, -12px); + -webkit-animation-delay:-0.45s; +} +.ajax-spinner-bars > .bar-9 { + -webkit-transform: rotate(180deg) translate(0, -12px); + -webkit-animation-delay:-0.4s; +} +.ajax-spinner-bars > .bar-10 { + -webkit-transform: rotate(202.5deg) translate(0, -12px); + -webkit-animation-delay:-0.35s; +} +.ajax-spinner-bars > .bar-11 { + -webkit-transform: rotate(225deg) translate(0, -12px); + -webkit-animation-delay:-0.3s; +} +.ajax-spinner-bars > .bar-12 { + -webkit-transform: rotate(247.5deg) translate(0, -12px); + -webkit-animation-delay:-0.25s; +} +.ajax-spinner-bars> .bar-13 { + -webkit-transform: rotate(270deg) translate(0, -12px); + -webkit-animation-delay:-0.2s; +} +.ajax-spinner-bars > .bar-14 { + -webkit-transform: rotate(292.5deg) translate(0, -12px); + -webkit-animation-delay:-0.15s; +} +.ajax-spinner-bars > .bar-15 { + -webkit-transform: rotate(315deg) translate(0, -12px); + -webkit-animation-delay:-0.1s; +} +.ajax-spinner-bars> .bar-16 { + -webkit-transform: rotate(337.5deg) translate(0, -12px); + -webkit-animation-delay:-0.05s; +} +@-webkit-keyframes fadeit{ + 0%{ opacity:1; } + 100%{ opacity:0;} +} \ No newline at end of file diff --git a/meteor/client/stylesheets/theme.import.less b/meteor/client/stylesheets/theme.import.less new file mode 100755 index 0000000000..ec8280f8c9 --- /dev/null +++ b/meteor/client/stylesheets/theme.import.less @@ -0,0 +1,243 @@ +// +// Base +// -------------------------------------------------- + +html, body { + height: 100%; + font-family: 'Helvetica', sans-serif; + font-weight: 300; + background-color: @brand-primary; + font-size: @font-size-base; + height: @window-height; + .no-select(); +} + +html, body, div, span, object, +form, input, h1, h2, button, label, a, img { + .no-select(); +} + +h1, h2, h3, h4, h5, h6 { + margin-top: 0; + font-weight: 300; + small { + margin-left: 0.6em; + } +} + +small, .text-muted { + color: @text-color-light; +} + +a { + color: @brand-action; + &:hover { + color: darken(@brand-action, 10%); + text-decoration: none; + } + &.active { + color: darken(@brand-action, 10%); + } +} + + +// +// Buttons +// -------------------------------------------------- + +.btn-styles(@btn-color: #555) { + border-radius: @border-radius-base; + background-color: @btn-color; + .reset-filter(); // Disable gradients for IE9 because filter bleeds through rounded corners + border-width: 1px; + transition: background-color 0.2s, color 0.2s, width 0.2s, border-width 0.2s, border-color 0.2s; +} + +.btn { + font-weight: 300; + outline: none; + &, + &:active, + &.active { + &:focus { + outline: none; + } + } + &:hover, + &:focus { + outline: none; + } + &:active, + &.active { + outline: none; + .box-shadow(~"inset 0 0 0 rgba(0,0,0,.075)"); + } +} + +button { + &.btn { + font-weight: 300; + } +} + +.btn-default { + .btn-styles(@btn-default-bg); + border-color: @btn-default-border; + border-width: 1px; + &:hover, + &:focus { + background-color: @btn-default-bg; + border-color: darken(@btn-default-border, 40%); + color: darken(@btn-default-color, 40%); + } + &:active, + &.active { + background-color: @btn-default-bg; + border-color: darken(@btn-default-border, 40%); + color: darken(@btn-default-color, 40%); + } +} + +.btn-action { + .btn-styles(@btn-action-bg); + color: @btn-action-color; + border-color: @btn-action-border; + &:hover, + &:focus { + border-color: darken(@btn-action-border, 10%); + color: darken(@btn-action-color, 10%); + } + &:active { + border-color: darken(@btn-action-border, 10%); + color: darken(@btn-action-color, 10%); + } + &.active { + background-color: @btn-action-color; + color: white; + } +} + +.btn-positive { + .btn-styles(@btn-positive-bg); + color: @btn-positive-color; + border-color: @btn-positive-border; + &:hover, + &:focus { + border-color: darken(@btn-positive-border, 10%); + color: darken(@btn-positive-color, 10%); + } + &:active, + &.active { + border-color: darken(@btn-positive-border, 10%); + color: darken(@btn-positive-color, 10%); + } +} + +.btn-negative { + .btn-styles(@btn-negative-bg); + color: @btn-negative-color; + border-color: @btn-negative-border; + &:hover, + &:focus { + border-color: darken(@btn-negative-border, 10%); + color: darken(@btn-negative-color, 10%); + } + &:active, + &.active { + border-color: darken(@btn-negative-border, 10%); + color: darken(@btn-negative-color, 10%); + } +} + +// +// Forms +// -------------------------------------------------- + +textarea:focus, +input[type="text"]:focus, +input[type="password"]:focus, +input[type="datetime"]:focus, +input[type="datetime-local"]:focus, +input[type="date"]:focus, +input[type="month"]:focus, +input[type="time"]:focus, +input[type="week"]:focus, +input[type="number"]:focus, +input[type="email"]:focus, +input[type="url"]:focus, +input[type="search"]:focus, +input[type="tel"]:focus, +input[type="color"]:focus, +.uneditable-input:focus { + border-color: @brand-action; + outline: none; + .box-shadow(~"inset 0 0 1px rgba(0,0,0,.075), 0 0 0 @{brand-action}"); +} + +.has-error { + .form-control:focus { + .box-shadow(~"inset 0 0 1px rgba(0,0,0,.075), 0 0 0 @{brand-negative}"); + } +} + +.has-success { + .form-control:focus { + .box-shadow(~"inset 0 0 1px rgba(0,0,0,.075), 0 0 0 @{brand-positive}"); + } +} + +.help-block { + font-size: 12px; +} + +// +// Modal +// -------------------------------------------------- + +.modal-header { + padding: 8px; + border-top-left-radius: @border-radius-base; + border-top-right-radius:@border-radius-base; + border-bottom: 1px solid @text-color-lightest; + color: @text-color-light; + text-align: center; + h5 { + font-size: 13px; + font-weight: 300; + } + .close { + position: absolute; + top: 10px; + right: 8px; + font-size: 18px; + color: @text-color-lighter; + opacity: 1; + &:focus { + outline: 0; + } + } +} + +.modal-body { + background-color: white; +} + +.modal-footer { + border-bottom-left-radius: @border-radius-base; + border-bottom-right-radius: @border-radius-base; + border-top: 1px solid @text-color-lightest; +} + +// +// Misc +// -------------------------------------------------- + +::selection { + color: white; + background-color: @selection-color; +} + +::-moz-selection { + color: white; + background-color: @selection-color; +} diff --git a/meteor/client/stylesheets/typicons/typicons.import.less b/meteor/client/stylesheets/typicons/typicons.import.less new file mode 100644 index 0000000000..df0bec54e1 --- /dev/null +++ b/meteor/client/stylesheets/typicons/typicons.import.less @@ -0,0 +1,1040 @@ +@charset 'UTF-8'; + +/* @FONT-FACE loads font into browser */ +@font-face { + font-family: 'typicons'; + font-weight: normal; + font-style: normal; + src: url('/fonts/typicons.eot'); + src: url('/fonts/typicons.eot?#iefix') format('embedded-opentype'), + url('/fonts/typicons.woff') format('woff'), + url('/fonts/typicons.ttf') format('truetype'), + url('/fonts/typicons.svg#typicons') format('svg'); +} + +/* :before psuedo-selector inserts and styles icon */ +.typcn:before { + font-family: 'typicons'; + font-style: normal; + font-weight: normal; + speak: none; + display: inline-block; + text-decoration: inherit; + width: 1.15em; + height: 1.15em; + font-size: 1.15em; + text-align: center; + -webkit-font-smoothing: antialiased; + font-smoothing: antialiased; + text-rendering: optimizeLegibility; +} + +/* Code for individual icons */ +.typcn-adjust-brightness:before { + content: '\e000'; /* '' */ +} +.typcn-adjust-contrast:before { + content: '\e001'; /* '' */ +} +.typcn-anchor-outline:before { + content: '\e002'; /* '' */ +} +.typcn-anchor:before { + content: '\e003'; /* '' */ +} +.typcn-archive:before { + content: '\e004'; /* '' */ +} +.typcn-arrow-back-outline:before { + content: '\e005'; /* '' */ +} +.typcn-arrow-back:before { + content: '\e006'; /* '' */ +} +.typcn-arrow-down-outline:before { + content: '\e007'; /* '' */ +} +.typcn-arrow-down-thick:before { + content: '\e008'; /* '' */ +} +.typcn-arrow-down:before { + content: '\e009'; /* '' */ +} +.typcn-arrow-forward-outline:before { + content: '\e00a'; /* '' */ +} +.typcn-arrow-forward:before { + content: '\e00b'; /* '' */ +} +.typcn-arrow-left-outline:before { + content: '\e00c'; /* '' */ +} +.typcn-arrow-left-thick:before { + content: '\e00d'; /* '' */ +} +.typcn-arrow-left:before { + content: '\e00e'; /* '' */ +} +.typcn-arrow-loop-outline:before { + content: '\e00f'; /* '' */ +} +.typcn-arrow-loop:before { + content: '\e010'; /* '' */ +} +.typcn-arrow-maximise-outline:before { + content: '\e011'; /* '' */ +} +.typcn-arrow-maximise:before { + content: '\e012'; /* '' */ +} +.typcn-arrow-minimise-outline:before { + content: '\e013'; /* '' */ +} +.typcn-arrow-minimise:before { + content: '\e014'; /* '' */ +} +.typcn-arrow-move-outline:before { + content: '\e015'; /* '' */ +} +.typcn-arrow-move:before { + content: '\e016'; /* '' */ +} +.typcn-arrow-repeat-outline:before { + content: '\e017'; /* '' */ +} +.typcn-arrow-repeat:before { + content: '\e018'; /* '' */ +} +.typcn-arrow-right-outline:before { + content: '\e019'; /* '' */ +} +.typcn-arrow-right-thick:before { + content: '\e01a'; /* '' */ +} +.typcn-arrow-right:before { + content: '\e01b'; /* '' */ +} +.typcn-arrow-shuffle:before { + content: '\e01c'; /* '' */ +} +.typcn-arrow-sorted-down:before { + content: '\e01d'; /* '' */ +} +.typcn-arrow-sorted-up:before { + content: '\e01e'; /* '' */ +} +.typcn-arrow-sync-outline:before { + content: '\e01f'; /* '' */ +} +.typcn-arrow-sync:before { + content: '\e020'; /* '' */ +} +.typcn-arrow-unsorted:before { + content: '\e021'; /* '' */ +} +.typcn-arrow-up-outline:before { + content: '\e022'; /* '' */ +} +.typcn-arrow-up-thick:before { + content: '\e023'; /* '' */ +} +.typcn-arrow-up:before { + content: '\e024'; /* '' */ +} +.typcn-at:before { + content: '\e025'; /* '' */ +} +.typcn-attachment-outline:before { + content: '\e026'; /* '' */ +} +.typcn-attachment:before { + content: '\e027'; /* '' */ +} +.typcn-backspace-outline:before { + content: '\e028'; /* '' */ +} +.typcn-backspace:before { + content: '\e029'; /* '' */ +} +.typcn-battery-charge:before { + content: '\e02a'; /* '' */ +} +.typcn-battery-full:before { + content: '\e02b'; /* '' */ +} +.typcn-battery-high:before { + content: '\e02c'; /* '' */ +} +.typcn-battery-low:before { + content: '\e02d'; /* '' */ +} +.typcn-battery-mid:before { + content: '\e02e'; /* '' */ +} +.typcn-beaker:before { + content: '\e02f'; /* '' */ +} +.typcn-beer:before { + content: '\e030'; /* '' */ +} +.typcn-bell:before { + content: '\e031'; /* '' */ +} +.typcn-book:before { + content: '\e032'; /* '' */ +} +.typcn-bookmark:before { + content: '\e033'; /* '' */ +} +.typcn-briefcase:before { + content: '\e034'; /* '' */ +} +.typcn-brush:before { + content: '\e035'; /* '' */ +} +.typcn-business-card:before { + content: '\e036'; /* '' */ +} +.typcn-calculator:before { + content: '\e037'; /* '' */ +} +.typcn-calendar-outline:before { + content: '\e038'; /* '' */ +} +.typcn-calendar:before { + content: '\e039'; /* '' */ +} +.typcn-camera-outline:before { + content: '\e03a'; /* '' */ +} +.typcn-camera:before { + content: '\e03b'; /* '' */ +} +.typcn-cancel-outline:before { + content: '\e03c'; /* '' */ +} +.typcn-cancel:before { + content: '\e03d'; /* '' */ +} +.typcn-chart-area-outline:before { + content: '\e03e'; /* '' */ +} +.typcn-chart-area:before { + content: '\e03f'; /* '' */ +} +.typcn-chart-bar-outline:before { + content: '\e040'; /* '' */ +} +.typcn-chart-bar:before { + content: '\e041'; /* '' */ +} +.typcn-chart-line-outline:before { + content: '\e042'; /* '' */ +} +.typcn-chart-line:before { + content: '\e043'; /* '' */ +} +.typcn-chart-pie-outline:before { + content: '\e044'; /* '' */ +} +.typcn-chart-pie:before { + content: '\e045'; /* '' */ +} +.typcn-chevron-left-outline:before { + content: '\e046'; /* '' */ +} +.typcn-chevron-left:before { + content: '\e047'; /* '' */ +} +.typcn-chevron-right-outline:before { + content: '\e048'; /* '' */ +} +.typcn-chevron-right:before { + content: '\e049'; /* '' */ +} +.typcn-clipboard:before { + content: '\e04a'; /* '' */ +} +.typcn-cloud-storage:before { + content: '\e04b'; /* '' */ +} +.typcn-cloud-storage-outline:before { + content: '\e054'; /* '' */ +} +.typcn-code-outline:before { + content: '\e04c'; /* '' */ +} +.typcn-code:before { + content: '\e04d'; /* '' */ +} +.typcn-coffee:before { + content: '\e04e'; /* '' */ +} +.typcn-cog-outline:before { + content: '\e04f'; /* '' */ +} +.typcn-cog:before { + content: '\e050'; /* '' */ +} +.typcn-compass:before { + content: '\e051'; /* '' */ +} +.typcn-contacts:before { + content: '\e052'; /* '' */ +} +.typcn-credit-card:before { + content: '\e053'; /* '' */ +} +.typcn-css3:before { + content: '\e055'; /* '' */ +} +.typcn-database:before { + content: '\e056'; /* '' */ +} +.typcn-delete-outline:before { + content: '\e057'; /* '' */ +} +.typcn-delete:before { + content: '\e058'; /* '' */ +} +.typcn-device-desktop:before { + content: '\e059'; /* '' */ +} +.typcn-device-laptop:before { + content: '\e05a'; /* '' */ +} +.typcn-device-phone:before { + content: '\e05b'; /* '' */ +} +.typcn-device-tablet:before { + content: '\e05c'; /* '' */ +} +.typcn-directions:before { + content: '\e05d'; /* '' */ +} +.typcn-divide-outline:before { + content: '\e05e'; /* '' */ +} +.typcn-divide:before { + content: '\e05f'; /* '' */ +} +.typcn-document-add:before { + content: '\e060'; /* '' */ +} +.typcn-document-delete:before { + content: '\e061'; /* '' */ +} +.typcn-document-text:before { + content: '\e062'; /* '' */ +} +.typcn-document:before { + content: '\e063'; /* '' */ +} +.typcn-download-outline:before { + content: '\e064'; /* '' */ +} +.typcn-download:before { + content: '\e065'; /* '' */ +} +.typcn-dropbox:before { + content: '\e066'; /* '' */ +} +.typcn-edit:before { + content: '\e067'; /* '' */ +} +.typcn-eject-outline:before { + content: '\e068'; /* '' */ +} +.typcn-eject:before { + content: '\e069'; /* '' */ +} +.typcn-equals-outline:before { + content: '\e06a'; /* '' */ +} +.typcn-equals:before { + content: '\e06b'; /* '' */ +} +.typcn-export-outline:before { + content: '\e06c'; /* '' */ +} +.typcn-export:before { + content: '\e06d'; /* '' */ +} +.typcn-eye-outline:before { + content: '\e06e'; /* '' */ +} +.typcn-eye:before { + content: '\e06f'; /* '' */ +} +.typcn-feather:before { + content: '\e070'; /* '' */ +} +.typcn-film:before { + content: '\e071'; /* '' */ +} +.typcn-filter:before { + content: '\e072'; /* '' */ +} +.typcn-flag-outline:before { + content: '\e073'; /* '' */ +} +.typcn-flag:before { + content: '\e074'; /* '' */ +} +.typcn-flash-outline:before { + content: '\e075'; /* '' */ +} +.typcn-flash:before { + content: '\e076'; /* '' */ +} +.typcn-flow-children:before { + content: '\e077'; /* '' */ +} +.typcn-flow-merge:before { + content: '\e078'; /* '' */ +} +.typcn-flow-parallel:before { + content: '\e079'; /* '' */ +} +.typcn-flow-switch:before { + content: '\e07a'; /* '' */ +} +.typcn-folder-add:before { + content: '\e07b'; /* '' */ +} +.typcn-folder-delete:before { + content: '\e07c'; /* '' */ +} +.typcn-folder-open:before { + content: '\e07d'; /* '' */ +} +.typcn-folder:before { + content: '\e07e'; /* '' */ +} +.typcn-gift:before { + content: '\e07f'; /* '' */ +} +.typcn-globe-outline:before { + content: '\e080'; /* '' */ +} +.typcn-globe:before { + content: '\e081'; /* '' */ +} +.typcn-group-outline:before { + content: '\e082'; /* '' */ +} +.typcn-group:before { + content: '\e083'; /* '' */ +} +.typcn-headphones:before { + content: '\e084'; /* '' */ +} +.typcn-heart-full-outline:before { + content: '\e085'; /* '' */ +} +.typcn-heart-half-outline:before { + content: '\e086'; /* '' */ +} +.typcn-heart-outline:before { + content: '\e087'; /* '' */ +} +.typcn-heart:before { + content: '\e088'; /* '' */ +} +.typcn-home-outline:before { + content: '\e089'; /* '' */ +} +.typcn-home:before { + content: '\e08a'; /* '' */ +} +.typcn-html5:before { + content: '\e08b'; /* '' */ +} +.typcn-image-outline:before { + content: '\e08c'; /* '' */ +} +.typcn-image:before { + content: '\e08d'; /* '' */ +} +.typcn-infinity-outline:before { + content: '\e08e'; /* '' */ +} +.typcn-infinity:before { + content: '\e08f'; /* '' */ +} +.typcn-info-large-outline:before { + content: '\e090'; /* '' */ +} +.typcn-info-large:before { + content: '\e091'; /* '' */ +} +.typcn-info-outline:before { + content: '\e092'; /* '' */ +} +.typcn-info:before { + content: '\e093'; /* '' */ +} +.typcn-input-checked-outline:before { + content: '\e094'; /* '' */ +} +.typcn-input-checked:before { + content: '\e095'; /* '' */ +} +.typcn-key-outline:before { + content: '\e096'; /* '' */ +} +.typcn-key:before { + content: '\e097'; /* '' */ +} +.typcn-keyboard:before { + content: '\e098'; /* '' */ +} +.typcn-leaf:before { + content: '\e099'; /* '' */ +} +.typcn-lightbulb:before { + content: '\e09a'; /* '' */ +} +.typcn-link-outline:before { + content: '\e09b'; /* '' */ +} +.typcn-link:before { + content: '\e09c'; /* '' */ +} +.typcn-location-arrow-outline:before { + content: '\e09d'; /* '' */ +} +.typcn-location-arrow:before { + content: '\e09e'; /* '' */ +} +.typcn-location-outline:before { + content: '\e09f'; /* '' */ +} +.typcn-location:before { + content: '\e0a0'; /* '' */ +} +.typcn-lock-closed-outline:before { + content: '\e0a1'; /* '' */ +} +.typcn-lock-closed:before { + content: '\e0a2'; /* '' */ +} +.typcn-lock-open-outline:before { + content: '\e0a3'; /* '' */ +} +.typcn-lock-open:before { + content: '\e0a4'; /* '' */ +} +.typcn-mail:before { + content: '\e0a5'; /* '' */ +} +.typcn-map:before { + content: '\e0a6'; /* '' */ +} +.typcn-media-eject-outline:before { + content: '\e0a7'; /* '' */ +} +.typcn-media-eject:before { + content: '\e0a8'; /* '' */ +} +.typcn-media-fast-forward-outline:before { + content: '\e0a9'; /* '' */ +} +.typcn-media-fast-forward:before { + content: '\e0aa'; /* '' */ +} +.typcn-media-pause-outline:before { + content: '\e0ab'; /* '' */ +} +.typcn-media-pause:before { + content: '\e0ac'; /* '' */ +} +.typcn-media-play-outline:before { + content: '\e0ad'; /* '' */ +} +.typcn-media-play-reverse-outline:before { + content: '\e0ae'; /* '' */ +} +.typcn-media-play-reverse:before { + content: '\e0af'; /* '' */ +} +.typcn-media-play:before { + content: '\e0b0'; /* '' */ +} +.typcn-media-record-outline:before { + content: '\e0b1'; /* '' */ +} +.typcn-media-record:before { + content: '\e0b2'; /* '' */ +} +.typcn-media-rewind-outline:before { + content: '\e0b3'; /* '' */ +} +.typcn-media-rewind:before { + content: '\e0b4'; /* '' */ +} +.typcn-media-stop-outline:before { + content: '\e0b5'; /* '' */ +} +.typcn-media-stop:before { + content: '\e0b6'; /* '' */ +} +.typcn-message-typing:before { + content: '\e0b7'; /* '' */ +} +.typcn-message:before { + content: '\e0b8'; /* '' */ +} +.typcn-messages:before { + content: '\e0b9'; /* '' */ +} +.typcn-microphone-outline:before { + content: '\e0ba'; /* '' */ +} +.typcn-microphone:before { + content: '\e0bb'; /* '' */ +} +.typcn-minus-outline:before { + content: '\e0bc'; /* '' */ +} +.typcn-minus:before { + content: '\e0bd'; /* '' */ +} +.typcn-mortar-board:before { + content: '\e0be'; /* '' */ +} +.typcn-news:before { + content: '\e0bf'; /* '' */ +} +.typcn-notes-outline:before { + content: '\e0c0'; /* '' */ +} +.typcn-notes:before { + content: '\e0c1'; /* '' */ +} +.typcn-pen:before { + content: '\e0c2'; /* '' */ +} +.typcn-pencil:before { + content: '\e0c3'; /* '' */ +} +.typcn-phone-outline:before { + content: '\e0c4'; /* '' */ +} +.typcn-phone:before { + content: '\e0c5'; /* '' */ +} +.typcn-pi-outline:before { + content: '\e0c6'; /* '' */ +} +.typcn-pi:before { + content: '\e0c7'; /* '' */ +} +.typcn-pin-outline:before { + content: '\e0c8'; /* '' */ +} +.typcn-pin:before { + content: '\e0c9'; /* '' */ +} +.typcn-pipette:before { + content: '\e0ca'; /* '' */ +} +.typcn-plane-outline:before { + content: '\e0cb'; /* '' */ +} +.typcn-plane:before { + content: '\e0cc'; /* '' */ +} +.typcn-plug:before { + content: '\e0cd'; /* '' */ +} +.typcn-plus-outline:before { + content: '\e0ce'; /* '' */ +} +.typcn-plus:before { + content: '\e0cf'; /* '' */ +} +.typcn-point-of-interest-outline:before { + content: '\e0d0'; /* '' */ +} +.typcn-point-of-interest:before { + content: '\e0d1'; /* '' */ +} +.typcn-power-outline:before { + content: '\e0d2'; /* '' */ +} +.typcn-power:before { + content: '\e0d3'; /* '' */ +} +.typcn-printer:before { + content: '\e0d4'; /* '' */ +} +.typcn-puzzle-outline:before { + content: '\e0d5'; /* '' */ +} +.typcn-puzzle:before { + content: '\e0d6'; /* '' */ +} +.typcn-radar-outline:before { + content: '\e0d7'; /* '' */ +} +.typcn-radar:before { + content: '\e0d8'; /* '' */ +} +.typcn-refresh-outline:before { + content: '\e0d9'; /* '' */ +} +.typcn-refresh:before { + content: '\e0da'; /* '' */ +} +.typcn-rss-outline:before { + content: '\e0db'; /* '' */ +} +.typcn-rss:before { + content: '\e0dc'; /* '' */ +} +.typcn-scissors-outline:before { + content: '\e0dd'; /* '' */ +} +.typcn-scissors:before { + content: '\e0de'; /* '' */ +} +.typcn-shopping-bag:before { + content: '\e0df'; /* '' */ +} +.typcn-shopping-cart:before { + content: '\e0e0'; /* '' */ +} +.typcn-social-at-circular:before { + content: '\e0e1'; /* '' */ +} +.typcn-social-dribbble-circular:before { + content: '\e0e2'; /* '' */ +} +.typcn-social-dribbble:before { + content: '\e0e3'; /* '' */ +} +.typcn-social-facebook-circular:before { + content: '\e0e4'; /* '' */ +} +.typcn-social-facebook:before { + content: '\e0e5'; /* '' */ +} +.typcn-social-flickr-circular:before { + content: '\e0e6'; /* '' */ +} +.typcn-social-flickr:before { + content: '\e0e7'; /* '' */ +} +.typcn-social-github-circular:before { + content: '\e0e8'; /* '' */ +} +.typcn-social-github:before { + content: '\e0e9'; /* '' */ +} +.typcn-social-google-plus-circular:before { + content: '\e0ea'; /* '' */ +} +.typcn-social-google-plus:before { + content: '\e0eb'; /* '' */ +} +.typcn-social-instagram-circular:before { + content: '\e0ec'; /* '' */ +} +.typcn-social-instagram:before { + content: '\e0ed'; /* '' */ +} +.typcn-social-last-fm-circular:before { + content: '\e0ee'; /* '' */ +} +.typcn-social-last-fm:before { + content: '\e0ef'; /* '' */ +} +.typcn-social-linkedin-circular:before { + content: '\e0f0'; /* '' */ +} +.typcn-social-linkedin:before { + content: '\e0f1'; /* '' */ +} +.typcn-social-pinterest-circular:before { + content: '\e0f2'; /* '' */ +} +.typcn-social-pinterest:before { + content: '\e0f3'; /* '' */ +} +.typcn-social-skype-outline:before { + content: '\e0f4'; /* '' */ +} +.typcn-social-skype:before { + content: '\e0f5'; /* '' */ +} +.typcn-social-tumbler-circular:before { + content: '\e0f6'; /* '' */ +} +.typcn-social-tumbler:before { + content: '\e0f7'; /* '' */ +} +.typcn-social-twitter-circular:before { + content: '\e0f8'; /* '' */ +} +.typcn-social-twitter:before { + content: '\e0f9'; /* '' */ +} +.typcn-social-vimeo-circular:before { + content: '\e0fa'; /* '' */ +} +.typcn-social-vimeo:before { + content: '\e0fb'; /* '' */ +} +.typcn-social-youtube-circular:before { + content: '\e0fc'; /* '' */ +} +.typcn-social-youtube:before { + content: '\e0fd'; /* '' */ +} +.typcn-sort-alphabetically-outline:before { + content: '\e0fe'; /* '' */ +} +.typcn-sort-alphabetically:before { + content: '\e0ff'; /* '' */ +} +.typcn-sort-numerically-outline:before { + content: '\e100'; /* '' */ +} +.typcn-sort-numerically:before { + content: '\e101'; /* '' */ +} +.typcn-spanner-outline:before { + content: '\e102'; /* '' */ +} +.typcn-spanner:before { + content: '\e103'; /* '' */ +} +.typcn-spiral:before { + content: '\e104'; /* '' */ +} +.typcn-star-full-outline:before { + content: '\e105'; /* '' */ +} +.typcn-star-half-outline:before { + content: '\e106'; /* '' */ +} +.typcn-star-half:before { + content: '\e107'; /* '' */ +} +.typcn-star-outline:before { + content: '\e108'; /* '' */ +} +.typcn-star:before { + content: '\e109'; /* '' */ +} +.typcn-starburst-outline:before { + content: '\e10a'; /* '' */ +} +.typcn-starburst:before { + content: '\e10b'; /* '' */ +} +.typcn-stopwatch:before { + content: '\e10c'; /* '' */ +} +.typcn-support:before { + content: '\e10d'; /* '' */ +} +.typcn-tabs-outline:before { + content: '\e10e'; /* '' */ +} +.typcn-tag:before { + content: '\e10f'; /* '' */ +} +.typcn-tags:before { + content: '\e110'; /* '' */ +} +.typcn-th-large-outline:before { + content: '\e111'; /* '' */ +} +.typcn-th-large:before { + content: '\e112'; /* '' */ +} +.typcn-th-list-outline:before { + content: '\e113'; /* '' */ +} +.typcn-th-list:before { + content: '\e114'; /* '' */ +} +.typcn-th-menu-outline:before { + content: '\e115'; /* '' */ +} +.typcn-th-menu:before { + content: '\e116'; /* '' */ +} +.typcn-th-small-outline:before { + content: '\e117'; /* '' */ +} +.typcn-th-small:before { + content: '\e118'; /* '' */ +} +.typcn-thermometer:before { + content: '\e119'; /* '' */ +} +.typcn-thumbs-down:before { + content: '\e11a'; /* '' */ +} +.typcn-thumbs-ok:before { + content: '\e11b'; /* '' */ +} +.typcn-thumbs-up:before { + content: '\e11c'; /* '' */ +} +.typcn-tick-outline:before { + content: '\e11d'; /* '' */ +} +.typcn-tick:before { + content: '\e11e'; /* '' */ +} +.typcn-ticket:before { + content: '\e11f'; /* '' */ +} +.typcn-time:before { + content: '\e120'; /* '' */ +} +.typcn-times-outline:before { + content: '\e121'; /* '' */ +} +.typcn-times:before { + content: '\e122'; /* '' */ +} +.typcn-trash:before { + content: '\e123'; /* '' */ +} +.typcn-tree:before { + content: '\e124'; /* '' */ +} +.typcn-upload-outline:before { + content: '\e125'; /* '' */ +} +.typcn-upload:before { + content: '\e126'; /* '' */ +} +.typcn-user-add-outline:before { + content: '\e127'; /* '' */ +} +.typcn-user-add:before { + content: '\e128'; /* '' */ +} +.typcn-user-delete-outline:before { + content: '\e129'; /* '' */ +} +.typcn-user-delete:before { + content: '\e12a'; /* '' */ +} +.typcn-user-outline:before { + content: '\e12b'; /* '' */ +} +.typcn-user:before { + content: '\e12c'; /* '' */ +} +.typcn-vendor-android:before { + content: '\e12d'; /* '' */ +} +.typcn-vendor-apple:before { + content: '\e12e'; /* '' */ +} +.typcn-vendor-microsoft:before { + content: '\e12f'; /* '' */ +} +.typcn-video-outline:before { + content: '\e130'; /* '' */ +} +.typcn-video:before { + content: '\e131'; /* '' */ +} +.typcn-volume-down:before { + content: '\e132'; /* '' */ +} +.typcn-volume-mute:before { + content: '\e133'; /* '' */ +} +.typcn-volume-up:before { + content: '\e134'; /* '' */ +} +.typcn-volume:before { + content: '\e135'; /* '' */ +} +.typcn-warning-outline:before { + content: '\e136'; /* '' */ +} +.typcn-warning:before { + content: '\e137'; /* '' */ +} +.typcn-watch:before { + content: '\e138'; /* '' */ +} +.typcn-waves-outline:before { + content: '\e139'; /* '' */ +} +.typcn-waves:before { + content: '\e13a'; /* '' */ +} +.typcn-weather-cloudy:before { + content: '\e13b'; /* '' */ +} +.typcn-weather-downpour:before { + content: '\e13c'; /* '' */ +} +.typcn-weather-night:before { + content: '\e13d'; /* '' */ +} +.typcn-weather-partly-sunny:before { + content: '\e13e'; /* '' */ +} +.typcn-weather-shower:before { + content: '\e13f'; /* '' */ +} +.typcn-weather-snow:before { + content: '\e140'; /* '' */ +} +.typcn-weather-stormy:before { + content: '\e141'; /* '' */ +} +.typcn-weather-sunny:before { + content: '\e142'; /* '' */ +} +.typcn-weather-windy-cloudy:before { + content: '\e143'; /* '' */ +} +.typcn-weather-windy:before { + content: '\e144'; /* '' */ +} +.typcn-wi-fi-outline:before { + content: '\e145'; /* '' */ +} +.typcn-wi-fi:before { + content: '\e146'; /* '' */ +} +.typcn-wine:before { + content: '\e147'; /* '' */ +} +.typcn-world-outline:before { + content: '\e148'; /* '' */ +} +.typcn-world:before { + content: '\e149'; /* '' */ +} +.typcn-zoom-in-outline:before { + content: '\e14a'; /* '' */ +} +.typcn-zoom-in:before { + content: '\e14b'; /* '' */ +} +.typcn-zoom-out-outline:before { + content: '\e14c'; /* '' */ +} +.typcn-zoom-out:before { + content: '\e14d'; /* '' */ +} +.typcn-zoom-outline:before { + content: '\e14e'; /* '' */ +} +.typcn-zoom:before { + content: '\e14f'; /* '' */ +} diff --git a/meteor/client/stylesheets/variables.import.less b/meteor/client/stylesheets/variables.import.less new file mode 100755 index 0000000000..a258b44eb4 --- /dev/null +++ b/meteor/client/stylesheets/variables.import.less @@ -0,0 +1,63 @@ +// +// Base +// -------------------------------------------------- + +@font-size-base: 14px; +@text-color: #343638; +@text-color-light: lighten(@text-color, 40%); +@text-color-lighter: lighten(@text-color, 60%); +@text-color-lightest: lighten(@text-color, 72%); + +@brand-action: #4A9AEC; +@brand-positive: #3AD86D; +@brand-negative: #F74B1F; + +@brand-primary: #343638; +@brand-secondary: #B5BFC7; + +@link-color: @brand-action; +@link-hover-color: darken(@link-color, 40%); + +@background-color: #F0F1F2; +@selection-color: rgba(red(@brand-action), green(@brand-action), blue(@brand-action), .5); + +@placeholder-color: #999; + +@border-radius-base: 4px; + +@window-width: 800px; +@window-height: 600px; + +// +// Buttons +// -------------------------------------------------- + +@btn-font-weight: normal; + +@btn-default-color: @text-color; +@btn-default-bg: white; +@btn-default-border: lighten(@text-color, 50%); + +@btn-action-color: @brand-action; +@btn-action-bg: white; +@btn-action-border: @brand-action; + +@btn-positive-color: @brand-positive; +@btn-positive-bg: white; +@btn-positive-border: @brand-positive; + +@btn-negative-color: @brand-negative; +@btn-negative-bg: white; +@btn-negative-border: @brand-negative; + +// +// Dashboard +// -------------------------------------------------- + +@dashboard-menu-bg-1: #5097D1; +@dashboard-menu-bg-2: #7AD2F6; + +@logs-background-color: #202225; + +@dashboard-header-height: 74px; +@dashboard-content-height: 493px; diff --git a/meteor/client/stylesheets/widgets.import.less b/meteor/client/stylesheets/widgets.import.less new file mode 100755 index 0000000000..93676baf99 --- /dev/null +++ b/meteor/client/stylesheets/widgets.import.less @@ -0,0 +1,111 @@ +.section { + .make-row(); + border-bottom: 1px solid @text-color-lightest; + margin: 1.6em; + &:last-child { + border-bottom: 0; + } + .left-section { + .make-xs-column(4); + padding-bottom: 1.6em; + .help-block { + color: @text-color-light; + } + } + .right-section { + .make-xs-column(8); + padding-bottom: 1.6em; + } +} + +form { + &.narrow { + max-width: 300px; + } + &.wide { + max-width: 500px; + } +} + +.inner-addon() { + position: relative; + i { + color: @placeholder-color; + position: absolute; + padding: 10px 12px; + pointer-events: none; + } +} + +.left-inner-addon { + .inner-addon(); + input { + padding-left: 36px; + } +} + +.right-inner-addon { + .inner-addon(); + input { + padding-right: 30px; + } + i { + right: 0; + } +} + +.line-select { + padding: 0.6em; + border-bottom: 1px solid @text-color-lightest; + cursor: pointer; + &:last-child { + border-bottom: 0; + } + h5 { + margin: 0; + } + .avatar { + position: relative; + top: -2px; + width: 20px; + height: 20px; + margin-right: 0.6em; + float: left; + } + .icon { + float: right; + position: relative; + top: -16px; + } + &:hover { + background-color: lighten(@text-color-lightest, 5%); + } +} + +.no-display { + display: none; +} + +.center { + text-align: center; +} + +.space-top { + margin-top: 1.2em; +} + +.space-bottom { + margin-bottom: 1.2em; +} + +.modal-small { + max-width: 400px; +} + +.error { + color: @brand-negative; + a { + font-weight: 600; + color: darken(@brand-negative, 10%); + } +} diff --git a/meteor/client/views/application/layout.html b/meteor/client/views/application/layout.html new file mode 100755 index 0000000000..1ceeafbe14 --- /dev/null +++ b/meteor/client/views/application/layout.html @@ -0,0 +1,5 @@ + diff --git a/meteor/client/views/dashboard/apps/dashboard-apps-logs.html b/meteor/client/views/dashboard/apps/dashboard-apps-logs.html new file mode 100644 index 0000000000..193d2db951 --- /dev/null +++ b/meteor/client/views/dashboard/apps/dashboard-apps-logs.html @@ -0,0 +1,7 @@ + diff --git a/meteor/client/views/dashboard/apps/dashboard-apps-settings.html b/meteor/client/views/dashboard/apps/dashboard-apps-settings.html new file mode 100755 index 0000000000..fff224a04e --- /dev/null +++ b/meteor/client/views/dashboard/apps/dashboard-apps-settings.html @@ -0,0 +1,71 @@ + diff --git a/meteor/client/views/dashboard/apps/dashboard-apps-settings.js b/meteor/client/views/dashboard/apps/dashboard-apps-settings.js new file mode 100755 index 0000000000..4b65f986d0 --- /dev/null +++ b/meteor/client/views/dashboard/apps/dashboard-apps-settings.js @@ -0,0 +1,52 @@ +var getConfigVars = function ($form) { + var configVars = {}; + $form.find('.env-var-pair').each(function () { + var envKey = $(this).find('.env-var-key').data('key'); + var envVal = $(this).find('.env-var-value').data('value'); + if (envKey) { + configVars[envKey] = envVal; + } + }); + return configVars; +}; + +Template.dashboard_apps_settings.events({ + 'click .btn-delete-var': function (e) { + var $button = $(e.currentTarget); + $button.attr("disabled", "disabled"); + var $form = $button.parents('.form-env-vars'); + var appId = $button.data('app-id'); + var envKey = $button.data('key'); + var configVars = getConfigVars($form); + delete configVars[envKey]; + Meteor.call('configVar', appId, configVars, function () { + $button.removeAttr('disabled'); + }); + }, + 'submit .form-env-vars': function (e) { + var $form = $(e.currentTarget); + var appId = this._id; + var configVars = getConfigVars($form); + var newKey = $form.find('input[name="env-var-key"]').val().trim(); + var newVal = $form.find('input[name="env-var-value"]').val().trim(); + if (newKey && newVal) { + configVars[newKey] = newVal; + Meteor.call('configVar', appId, configVars, function () { + $form.find('input[name="env-var-key"]').val(''); + $form.find('input[name="env-var-value"]').val(''); + }); + } + e.preventDefault(); + e.stopPropagation(); + return false; + }, + 'click .btn-delete-app': function () { + var result = confirm("Are you sure you want to delete this app?"); + if (result === true) { + Meteor.call('deleteApp', this._id, function (err) { + if (err) { throw err; } + }); + Router.go('dashboard_apps'); + } + } +}); diff --git a/meteor/client/views/dashboard/apps/dashboard-apps.html b/meteor/client/views/dashboard/apps/dashboard-apps.html new file mode 100755 index 0000000000..7831bfbe7e --- /dev/null +++ b/meteor/client/views/dashboard/apps/dashboard-apps.html @@ -0,0 +1,33 @@ + diff --git a/meteor/client/views/dashboard/apps/dashboard-apps.js b/meteor/client/views/dashboard/apps/dashboard-apps.js new file mode 100755 index 0000000000..d71c0c7520 --- /dev/null +++ b/meteor/client/views/dashboard/apps/dashboard-apps.js @@ -0,0 +1,5 @@ +Template.dashboard_apps.helpers({ + apps: function () { + return Apps.find({}, {sort: {createdAt: -1}}); + } +}); diff --git a/meteor/client/views/dashboard/apps/dashboard-single-app.html b/meteor/client/views/dashboard/apps/dashboard-single-app.html new file mode 100755 index 0000000000..f1e3740ff8 --- /dev/null +++ b/meteor/client/views/dashboard/apps/dashboard-single-app.html @@ -0,0 +1,33 @@ + diff --git a/meteor/client/views/dashboard/apps/dashboard-single-app.js b/meteor/client/views/dashboard/apps/dashboard-single-app.js new file mode 100755 index 0000000000..acbd198e79 --- /dev/null +++ b/meteor/client/views/dashboard/apps/dashboard-single-app.js @@ -0,0 +1,51 @@ +var path = require('path'); + +Template.dashboard_single_app.rendered = function () { + Meteor.setInterval(function () { + $('.btn-icon').tooltip(); + }, 1000); +}; + +Template.dashboard_single_app.events({ + 'click .btn-view': function (e) { + try { + var open = require('open'); + e.preventDefault(); + e.stopPropagation(); + var $btn = $(e.currentTarget); + var url = $btn.attr('href'); + open(url); + } catch (exception) { + console.log(exception); + } + }, + 'click .btn-terminal': function () { + var app = this; + var cmd = path.join(getBinDir(), 'boot2docker') + ' ssh -t "sudo docker-enter ' + app.docker.Id + '"'; + var terminalCmd = path.join(getBinDir(), 'terminal') + ' ' + cmd; + var exec = require('child_process').exec; + console.log(terminalCmd); + exec(terminalCmd, function (err, stdout) { + console.log(stdout); + if (err) { + console.log(err); + } + }); + }, + 'click .btn-restart': function () { + Meteor.call('restartApp', this._id, function (err) { + if (err) { throw err; } + }); + }, + 'click .btn-folder': function () { + var exec = require('child_process').exec; + exec('open ' + this.path, function (err) { + if (err) { throw err; } + }); + }, + 'click .btn-logs': function () { + Meteor.call('getAppLogs', this._id, function (err) { + if (err) { throw err; } + }); + } +}); diff --git a/meteor/client/views/dashboard/components/dashboard-menu.html b/meteor/client/views/dashboard/components/dashboard-menu.html new file mode 100755 index 0000000000..316f3cc9af --- /dev/null +++ b/meteor/client/views/dashboard/components/dashboard-menu.html @@ -0,0 +1,20 @@ + diff --git a/meteor/client/views/dashboard/components/dashboard-menu.js b/meteor/client/views/dashboard/components/dashboard-menu.js new file mode 100755 index 0000000000..71c325f59b --- /dev/null +++ b/meteor/client/views/dashboard/components/dashboard-menu.js @@ -0,0 +1,24 @@ +Template.dashboard_menu.events({ + 'click .mac-close': function () { + win.close(); + }, + 'click .mac-minimize': function () { + win.minimize(); + }, + 'mouseover .mac-window-options': function () { + $('.mac-close i').show(); + $('.mac-minimize i').show(); + $('.mac-maximize i').show(); + }, + 'mouseleave .mac-window-options': function () { + $('.mac-close i').hide(); + $('.mac-minimize i').hide(); + $('.mac-maximize i').hide(); + } +}); + +Template.dashboard_menu.rendered = function () { + $('.nav a').attr('tabIndex', '-1'); + $('.nav a').attr('onfocus', 'this.blur()'); + $('.nav a').tooltip(); +}; diff --git a/meteor/client/views/dashboard/components/menu-header.html b/meteor/client/views/dashboard/components/menu-header.html new file mode 100644 index 0000000000..7758802fab --- /dev/null +++ b/meteor/client/views/dashboard/components/menu-header.html @@ -0,0 +1,3 @@ + diff --git a/meteor/client/views/dashboard/components/modal-create-app.html b/meteor/client/views/dashboard/components/modal-create-app.html new file mode 100755 index 0000000000..61c4ec0014 --- /dev/null +++ b/meteor/client/views/dashboard/components/modal-create-app.html @@ -0,0 +1,59 @@ + diff --git a/meteor/client/views/dashboard/components/modal-create-app.js b/meteor/client/views/dashboard/components/modal-create-app.js new file mode 100755 index 0000000000..1458d0566d --- /dev/null +++ b/meteor/client/views/dashboard/components/modal-create-app.js @@ -0,0 +1,55 @@ +Template.modal_create_app.helpers({ + images: function () { + return Images.find({status: 'READY'}, {sort: {createdAt: -1}}); + } +}); + +Template.modal_create_app.events({ + 'submit #form-create-app': function (e) { + var $form = $(e.currentTarget); + var formData = $form.serializeObject(); + Meteor.call('formCreateApp', formData, function (errors, cleaned) { + if (errors) { + clearFormErrors($form); + showFormErrors($form, errors.details); + } else { + clearFormErrors($form); + Meteor.call('createApp', cleaned, function (err) { + if (err) { throw err; } + }); + $('#modal-create-app').bind('hidden.bs.modal', function () { + $('#slug-create-app-name').html(''); + resetForm($form); + $('#image-picker').find('.fa-check-square-o').hide(); + $('#image-picker').find('.fa-square-o').show(); + Router.go('dashboard_apps'); + }).modal('hide'); + } + }); + e.preventDefault(); + e.stopPropagation(); + return false; + }, + 'keyup #form-create-app input[name="name"]': function (e) { + var $input = $(e.currentTarget); + var slug = _($input.val()).slugify(); + if (slug) { + $('#slug-create-app-name').html('Name will be created as: ' + slug + ''); + } else { + $('#slug-create-app-name').html(''); + } + }, + 'click .pick-image': function (e) { + var $btn = $(e.currentTarget); + $('#form-create-app').find('input[name="imageId"]').val(this._id); + $('#image-picker').find('.fa-check-square-o').hide(); + $('#image-picker').find('.fa-square-o').show(); + $btn.find('.fa-square-o').hide(); + $btn.find('.fa-check-square-o').show(); + }, + 'click .btn-create-image': function () { + $('#modal-create-app').bind('hidden.bs.modal', function () { + $('#modal-create-image').modal('show'); + }).modal('hide'); + } +}); diff --git a/meteor/client/views/dashboard/components/modal-create-image.html b/meteor/client/views/dashboard/components/modal-create-image.html new file mode 100755 index 0000000000..1f6f981dd6 --- /dev/null +++ b/meteor/client/views/dashboard/components/modal-create-image.html @@ -0,0 +1,25 @@ + diff --git a/meteor/client/views/dashboard/components/modal-create-image.js b/meteor/client/views/dashboard/components/modal-create-image.js new file mode 100755 index 0000000000..25e16aac51 --- /dev/null +++ b/meteor/client/views/dashboard/components/modal-create-image.js @@ -0,0 +1,45 @@ +Template.modal_create_image.rendered = function () { + $('#modal-create-image').bind('hidden.bs.modal', function () { + Router.go('dashboard_images'); + }); +}; + +Template.modal_create_image.helpers({ + githubConfig: function () { + return ServiceConfiguration.configurations.findOne({service: 'github'}); + } +}); + +Template.modal_create_image.events({ + 'click #btn-pick-directory': function () { + $('#directory-picker').click(); + }, + 'change #directory-picker': function (e) { + var $picker = $(e.currentTarget); + var pickedDirectory = $picker.val(); + $('#picked-directory-error').html(''); + if (pickedDirectory) { + $('#picked-directory').html('' + pickedDirectory + ''); + Meteor.call('validateDirectory', pickedDirectory, function (err) { + if (err) { + $('#picked-directory-error').html(err.reason); + $('#btn-create-image').attr('disabled', 'disabled'); + } else { + $('#btn-create-image').removeAttr('disabled'); + } + }); + } else { + $('#picked-directory').html(''); + $('#btn-create-image').attr('disabled', 'disabled'); + } + }, + 'click #btn-create-image': function () { + var pickedDirectory = $('#directory-picker').val(); + $('#directory-picker').val(''); + $('#picked-directory-error').html(''); + $('#picked-directory').html(''); + $('#btn-create-image').attr('disabled', 'disabled'); + $('#modal-create-image').modal('hide'); + Meteor.call('createImage', pickedDirectory); + } +}); diff --git a/meteor/client/views/dashboard/helpers.js b/meteor/client/views/dashboard/helpers.js new file mode 100755 index 0000000000..afeeec426f --- /dev/null +++ b/meteor/client/views/dashboard/helpers.js @@ -0,0 +1,14 @@ +Handlebars.registerHelper('activeDashboardMenuItem', function (page) { + var currentPage = Router.current(true).path.split('/')[1]; + return page === currentPage ? 'active' : ''; +}); + +Handlebars.registerHelper('activeDashboardSubMenuItem', function (page) { + var currentPage = Router.current(true).path.split('/')[3]; + return page === currentPage ? 'active' : ''; +}); + +Handlebars.registerHelper('currentDashboardPage', function () { + var currentPage = Router.current(true).path.split('/')[1]; + return currentPage; +}); diff --git a/meteor/client/views/dashboard/images/dashboard-images-logs.html b/meteor/client/views/dashboard/images/dashboard-images-logs.html new file mode 100755 index 0000000000..fed85f3742 --- /dev/null +++ b/meteor/client/views/dashboard/images/dashboard-images-logs.html @@ -0,0 +1,18 @@ + diff --git a/meteor/client/views/dashboard/images/dashboard-images-settings.html b/meteor/client/views/dashboard/images/dashboard-images-settings.html new file mode 100755 index 0000000000..899ecba44b --- /dev/null +++ b/meteor/client/views/dashboard/images/dashboard-images-settings.html @@ -0,0 +1,78 @@ + diff --git a/meteor/client/views/dashboard/images/dashboard-images-settings.js b/meteor/client/views/dashboard/images/dashboard-images-settings.js new file mode 100755 index 0000000000..9021d22a34 --- /dev/null +++ b/meteor/client/views/dashboard/images/dashboard-images-settings.js @@ -0,0 +1,35 @@ +Template.dashboard_images_settings.events({ + 'click .btn-delete-image': function () { + var result = confirm("Are you sure you want to delete this image?"); + if (result === true) { + Meteor.call('deleteImage', this._id, function (err) { + if (err) { + $('#error-delete-image').html('' + err.reason + ''); + $('#error-delete-image').fadeIn(); + } else { + Router.go('dashboard_images'); + } + }); + } + }, + 'click #btn-pick-directory': function () { + $('#directory-picker').click(); + }, + 'change #directory-picker': function (e) { + var imageId = this._id; + var $picker = $(e.currentTarget); + var pickedDirectory = $picker.val(); + $('#picked-directory-error').html(''); + if (pickedDirectory) { + Meteor.call('validateDirectory', pickedDirectory, function (err) { + if (err) { + $('#picked-directory-error').html(err.reason); + } else { + Meteor.call('changeDirectory', imageId, pickedDirectory, function (err) { + if (err) { throw err; } + }); + } + }); + } + } +}); diff --git a/meteor/client/views/dashboard/images/dashboard-images.html b/meteor/client/views/dashboard/images/dashboard-images.html new file mode 100755 index 0000000000..ea1d5f837c --- /dev/null +++ b/meteor/client/views/dashboard/images/dashboard-images.html @@ -0,0 +1,33 @@ + diff --git a/meteor/client/views/dashboard/images/dashboard-images.js b/meteor/client/views/dashboard/images/dashboard-images.js new file mode 100755 index 0000000000..9edaddcd94 --- /dev/null +++ b/meteor/client/views/dashboard/images/dashboard-images.js @@ -0,0 +1,5 @@ +Template.dashboard_images.helpers({ + images: function () { + return Images.find({}, {sort: {createdAt: -1}}); + } +}); diff --git a/meteor/client/views/dashboard/images/dashboard-single-image.html b/meteor/client/views/dashboard/images/dashboard-single-image.html new file mode 100755 index 0000000000..427547d4ec --- /dev/null +++ b/meteor/client/views/dashboard/images/dashboard-single-image.html @@ -0,0 +1,40 @@ + diff --git a/meteor/client/views/dashboard/images/dashboard-single-image.js b/meteor/client/views/dashboard/images/dashboard-single-image.js new file mode 100755 index 0000000000..b6c44f1b27 --- /dev/null +++ b/meteor/client/views/dashboard/images/dashboard-single-image.js @@ -0,0 +1,25 @@ +Template.dashboard_single_image.rendered = function () { + Meteor.setInterval(function () { + $('.btn-icon').tooltip(); + }, 1000); +}; + +Template.dashboard_single_image.events({ + 'click .btn-create-app': function () { + $('#modal-create-app').modal('show'); + $('#form-create-app').find('input[name="imageId"]').val(this._id); + $('#image-picker').hide(); + }, + 'click .btn-folder': function () { + var exec = require('child_process').exec; + exec('open ' + this.originPath, function (err) { + if (err) { throw err; } + }); + }, + 'click .btn-rebuild': function () { + $('.btn-icon').tooltip('hide'); + Meteor.call('rebuildImage', this._id, function (err) { + if (err) { throw err; } + }); + } +}); diff --git a/meteor/client/views/dashboard/layouts/dashboard-apps-layout.html b/meteor/client/views/dashboard/layouts/dashboard-apps-layout.html new file mode 100755 index 0000000000..7f35bc79e3 --- /dev/null +++ b/meteor/client/views/dashboard/layouts/dashboard-apps-layout.html @@ -0,0 +1,37 @@ + diff --git a/meteor/client/views/dashboard/layouts/dashboard-apps-layout.js b/meteor/client/views/dashboard/layouts/dashboard-apps-layout.js new file mode 100644 index 0000000000..ae4b40de04 --- /dev/null +++ b/meteor/client/views/dashboard/layouts/dashboard-apps-layout.js @@ -0,0 +1,55 @@ +Template.dashboard_apps_layout.rendered = function () { + Meteor.setInterval(function () { + $('.header .icons a').tooltip(); + }, 1000); +}; + +Template.dashboard_apps_layout.events({ + 'click .btn-view': function (e) { + try { + var open = require('open'); + e.preventDefault(); + e.stopPropagation(); + var $btn = $(e.currentTarget); + var url = $btn.attr('href'); + open(url); + } catch (exception) { + console.log(exception); + } + }, + 'click .btn-image': function () { + $('.header .icons a').tooltip('hide'); + }, + 'click .btn-logs': function () { + Meteor.call('getAppLogs', this._id, function (err) { + if (err) { throw err; } + }); + }, + 'click .btn-terminal': function () { + var buildCmd = function (dockerId, termApp) { + return "echo 'boot2docker --vm=\"boot2docker-vm\" ssh -t \"sudo docker-enter " + dockerId + "\"' > /tmp/nsenter-start && chmod +x /tmp/nsenter-start && open -a " + termApp + " /tmp/nsenter-start"; + }; + var app = this; + var nsenterCmd = buildCmd(app.docker.Id, 'iTerm.app'); + var exec = require('child_process').exec; + exec(nsenterCmd, function (err) { + if (err) { + nsenterCmd = buildCmd(app.docker.Id, 'Terminal.app'); + exec(nsenterCmd, function (err) { + if (err) { throw err; } + }); + } + }); + }, + 'click .btn-restart': function () { + Meteor.call('restartApp', this._id, function (err) { + if (err) { throw err; } + }); + }, + 'click .btn-folder': function () { + var exec = require('child_process').exec; + exec('open ' + this.path, function (err) { + if (err) { throw err; } + }); + } +}); diff --git a/meteor/client/views/dashboard/layouts/dashboard-images-layout.html b/meteor/client/views/dashboard/layouts/dashboard-images-layout.html new file mode 100755 index 0000000000..00cc525b7e --- /dev/null +++ b/meteor/client/views/dashboard/layouts/dashboard-images-layout.html @@ -0,0 +1,36 @@ + diff --git a/meteor/client/views/dashboard/layouts/dashboard-images-layout.js b/meteor/client/views/dashboard/layouts/dashboard-images-layout.js new file mode 100755 index 0000000000..78e84305c3 --- /dev/null +++ b/meteor/client/views/dashboard/layouts/dashboard-images-layout.js @@ -0,0 +1,25 @@ +Template.dashboard_images_layout.rendered = function () { + Meteor.setInterval(function () { + $('.header .icons a').tooltip(); + }, 1000); +}; + +Template.dashboard_images_layout.events({ + 'click .btn-create-app': function () { + $('#modal-create-app').modal('show'); + $('#form-create-app').find('input[name="imageId"]').val(this._id); + $('#image-picker').hide(); + }, + 'click .btn-folder': function () { + var exec = require('child_process').exec; + exec('open ' + this.originPath, function (err) { + if (err) { throw err; } + }); + }, + 'click .btn-rebuild': function () { + $('.header .icons a').tooltip('hide'); + Meteor.call('rebuildImage', this._id, function (err) { + if (err) { throw err; } + }); + } +}); diff --git a/meteor/client/views/dashboard/layouts/dashboard-layout.html b/meteor/client/views/dashboard/layouts/dashboard-layout.html new file mode 100755 index 0000000000..62061490d2 --- /dev/null +++ b/meteor/client/views/dashboard/layouts/dashboard-layout.html @@ -0,0 +1,16 @@ + diff --git a/meteor/client/views/dashboard/settings/dashboard-settings.html b/meteor/client/views/dashboard/settings/dashboard-settings.html new file mode 100644 index 0000000000..9b3f6adc3f --- /dev/null +++ b/meteor/client/views/dashboard/settings/dashboard-settings.html @@ -0,0 +1,42 @@ + diff --git a/meteor/client/views/dashboard/settings/dashboard-settings.js b/meteor/client/views/dashboard/settings/dashboard-settings.js new file mode 100644 index 0000000000..cf12f91c27 --- /dev/null +++ b/meteor/client/views/dashboard/settings/dashboard-settings.js @@ -0,0 +1,28 @@ +Template.dashboard_settings.events({ + 'click .btn-start-boot2docker': function (e) { + var $btn = $(e.currentTarget); + $btn.html('Starting Boot2Docker...'); + $btn.attr("disabled", "disabled"); + startFixInterval(); + startBoot2Docker(function (err) { + if (err) { console.error(err); } + }); + }, + 'click .btn-stop-boot2docker': function (e) { + var $btn = $(e.currentTarget); + $btn.html('Stopping Boot2Docker...'); + $btn.attr("disabled", "disabled"); + stopFixInterval(); + stopBoot2Docker(function (err) { + if (err) { console.error(err); } + }); + } +}); + +Template.dashboard_settings.memory = function () { + return Session.get('boot2dockerMemoryUsage'); +}; + +Template.dashboard_settings.disk = function () { + return Session.get('boot2dockerDiskUsage'); +}; diff --git a/meteor/client/views/dashboard/setup/setup-install.html b/meteor/client/views/dashboard/setup/setup-install.html new file mode 100644 index 0000000000..62f666f6fa --- /dev/null +++ b/meteor/client/views/dashboard/setup/setup-install.html @@ -0,0 +1,41 @@ + diff --git a/meteor/client/views/dashboard/setup/setup-install.js b/meteor/client/views/dashboard/setup/setup-install.js new file mode 100644 index 0000000000..5ff517fa80 --- /dev/null +++ b/meteor/client/views/dashboard/setup/setup-install.js @@ -0,0 +1,145 @@ +var async = require('async'); + +// Install steps. A step is a function that accepts a function (err) callback and returns once that step is complete. +// keys: +// - install: Function that runs the installation step and calls the callback with an error if failed. +// - pastMessage: Message to show after step completion +// - message: Message to show while step is running +// - imperativeMessage: Message to show before running +var steps = [ + + // Step 0, set up VirtualBox + { + install: function (callback) { + isVirtualBoxInstalled(function (err, virtualBoxInstalled) { + setupVirtualBoxAndResolver(virtualBoxInstalled, function () { + callback(); + }); + }); + }, + pastMessage: 'VirtualBox installed', + message: 'Installing VirtualBox', + imperativeMessage: 'Install VirtualBox if necessary' + }, + + // Step 1: Set up the VM for running Kitematic apps + { + install: function (callback) { + console.log('Checking if vm exists...'); + boot2DockerVMExists(function (err, exists) { + console.log('VM exists: ' + exists); + if (exists) { + console.log('Stopping vm'); + stopBoot2Docker(function () { + console.log('Upgrading vm'); + upgradeBoot2Docker(function () { + callback(); + }); + }); + } else { + console.log('init VM'); + initBoot2Docker(function () { + callback(); + }); + } + }); + }, + pastMessage: 'Set up the Kitematic VM', + message: 'Setting up the Kitematic VM...', + imperativeMessage: 'Set up the Kitematic VM' + }, + + // Step 2: Start the Kitematic VM + { + install: function (callback) { + startBoot2Docker(function (err) { + callback(err); + }); + }, + pastMessage: 'Started the Kitematic VM', + message: 'Starting the Kitematic VM...', + imperativeMessage: 'Start the Kitematic VM' + }, + + // Step 3: Set up the default Kitematic images + { + install: function (callback) { + Meteor.call('reloadDefaultContainers', function (err) { + callback(err); + }); + }, + pastMessage: 'Started the Kitematic VM', + message: 'Setting up the default Kitematic images...', + imperativeMessage: 'Set up the default Kitematic images' + } +]; + +runSetup = function (callback) { + // Run through the Kitematic installation, skipping steps if required. + var currentStep = 0; + Session.set('currentInstallStep', currentStep); + Session.set('numberOfInstallSteps', steps.length); + async.eachSeries(steps, function (step, callback) { + console.log('Performing step ' + currentStep); + step.install(function (err) { + if (err) { + callback(err); + } else { + currentStep += 1; + Session.set('currentInstallStep', currentStep); + callback(); + } + }); + }, function (err) { + if (err) { + // if any of the steps fail + console.log('Kitematic setup failed at step' + currentStep); + console.log(err); + callback(err); + } else { + // Setup Finished + console.log('Setup finished.'); + callback(); + } + }); +}; + +var installStarted = false; +Template.setup_install.rendered = function() { + if(!installStarted) { + installStarted = true; + runSetup(function (err) { + if (err) { + console.log('Setup failed.'); + console.log(err); + } else { + Installs.insert({}); + startFixInterval(); + Router.go('dashboard_apps'); + } + }); + } +}; + +Template.setup_install.events({ + +}); + +Template.setup_install.steps = function () { + return steps.map(function (step, index) { + step.index = index; + return step; + }); +}; + +Template.setup_install.helpers({ + currentInstallStep: function () { + return Session.get('currentInstallStep'); + } +}); + +Template.setup_install.helpers({ + installComplete: function () { + return Session.get('currentInstallStep') === steps.length; + } +}); diff --git a/meteor/client/views/dashboard/setup/setup-intro.html b/meteor/client/views/dashboard/setup/setup-intro.html new file mode 100644 index 0000000000..8b031a0e7d --- /dev/null +++ b/meteor/client/views/dashboard/setup/setup-intro.html @@ -0,0 +1,13 @@ + diff --git a/meteor/client/views/dashboard/setup/setup-intro.js b/meteor/client/views/dashboard/setup/setup-intro.js new file mode 100644 index 0000000000..035945b7ec --- /dev/null +++ b/meteor/client/views/dashboard/setup/setup-intro.js @@ -0,0 +1,9 @@ +Template.setup_intro.events({ + 'click .continue-button': function (e) { + Router.go('setup_install'); + e.preventDefault(); + e.stopPropagation(); + return false; + } +}); + diff --git a/meteor/client/views/dashboard/setup/setup-layout.html b/meteor/client/views/dashboard/setup/setup-layout.html new file mode 100644 index 0000000000..12c0f8682b --- /dev/null +++ b/meteor/client/views/dashboard/setup/setup-layout.html @@ -0,0 +1,8 @@ + diff --git a/meteor/client/views/includes/spinner.html b/meteor/client/views/includes/spinner.html new file mode 100644 index 0000000000..c805b754fc --- /dev/null +++ b/meteor/client/views/includes/spinner.html @@ -0,0 +1,20 @@ + \ No newline at end of file diff --git a/meteor/collections/apps.js b/meteor/collections/apps.js new file mode 100755 index 0000000000..9ac599e4ce --- /dev/null +++ b/meteor/collections/apps.js @@ -0,0 +1,101 @@ +Apps = new Meteor.Collection('apps'); + +schemaApps = new SimpleSchema({ + imageId: { + type: Meteor.ObjectID, + label: "ID of the image used by the app", + max: 200 + }, + docker: { + type: Object, + label: "Docker container data", + blackbox: true, + optional: true + }, + status: { + type: String, + allowedValues: ['STARTING', 'READY', 'ERROR'], + label: "App current status", + max: 200 + }, + config: { + type: Object, + label: "App environment variables", + blackbox: true + }, + name: { + type: String, + label: "App name", + max: 200 + }, + logs: { + type: [String], + label: "Logs", + defaultValue: [] + }, + path: { + type: String, + label: "Path to the app directory", + optional: true + }, + createdAt: { + type: Date, + autoValue: function() { + var now = new Date(); + if (this.isInsert) { + return now; + } else if (this.isUpsert) { + return {$setOnInsert: now}; + } else { + this.unset(); + } + }, + denyUpdate: true, + label: "Time of app created" + } +}); + +Apps.helpers({ + image: function () { + return Images.findOne(this.imageId); + }, + url: function () { + var app = this; + var image = Images.findOne(app.imageId); + if (image && image.meta.app && image.meta.app.webPort) { + return 'http://' + app.name + '.dev:' + image.meta.app.webPort; + } else { + // Picks the best port + if (app.docker && app.docker.NetworkSettings.Ports) { + var keys = _.keys(app.docker.NetworkSettings.Ports); + var pickedPort = null; + _.each(keys, function (key) { + var port = parseInt(key.split('/')[0], 10); + if (_.contains(COMMON_WEB_PORTS, port) && port !== 22) { + pickedPort = port; + } + }); + if (pickedPort) { + return 'http://' + app.name + '.dev:' + pickedPort; + } else { + if (keys.length > 0) { + // Picks the first port that's not SSH + for (var i = 0; i < keys.length; i++) { + var port = parseInt(keys[i].split('/')[0], 10); + if (port !== 22) { + return 'http://' + app.name + '.dev:' + port; + } + } + return null; + } else { + return null; + } + } + } else { + return null; + } + } + } +}); + +Apps.attachSchema(schemaApps); diff --git a/meteor/collections/images.js b/meteor/collections/images.js new file mode 100755 index 0000000000..15055ad021 --- /dev/null +++ b/meteor/collections/images.js @@ -0,0 +1,93 @@ +Images = new Meteor.Collection('images'); + +schemaImages = new SimpleSchema({ + path: { + type: String, + label: "Path to the image directory", + optional: true + }, + originPath: { + type: String, + label: "Path to the folder where image is built from", + optional: true + }, + logoPath: { + type: String, + label: "Path to the image logo", + optional: true + }, + meta: { + type: Object, + label: "Meta data for the image", + blackbox: true, + optional: true + }, + docker: { + type: Object, + label: "Docker image data", + blackbox: true, + optional: true + }, + status: { + type: String, + allowedValues: ['BUILDING', 'READY', 'ERROR'], + label: "Image build current status", + max: 200 + }, + buildLogs: { + type: [String], + label: "Build logs", + defaultValue: [] + }, + createdAt: { + type: Date, + autoValue: function() { + var now = new Date(); + if (this.isInsert) { + return now; + } else if (this.isUpsert) { + return {$setOnInsert: now}; + } else { + this.unset(); + } + }, + denyUpdate: true, + label: "Time of image created" + } +}); + +Images.helpers({ + downloadStatus: function () { + if (this.buildLogs.length > 0) { + return _.last(this.buildLogs); + } else { + return null; + } + }, + downloadPercentage: function () { + if (this.buildLogs.length > 0) { + var lastLine = _.last(this.buildLogs); + if (_.last(lastLine) === '%') { + return _.last(lastLine.split(' ')); + } else { + return '100%'; + } + } else { + return '100%'; + } + } +}); + +Images.allow({ + 'update': function () { + return true; + }, + 'insert': function () { + return true; + }, + 'remove': function () { + return true; + } +}); + +Images.attachSchema(schemaImages); diff --git a/meteor/collections/installs.js b/meteor/collections/installs.js new file mode 100644 index 0000000000..6f0a492ce7 --- /dev/null +++ b/meteor/collections/installs.js @@ -0,0 +1,19 @@ +Installs = new Meteor.Collection('installs'); + +schemaInstalls = new SimpleSchema({ + +}); + +Installs.allow({ + 'update': function () { + return true; + }, + 'insert': function () { + return true; + }, + 'remove': function () { + return true; + } +}); + +Installs.attachSchema(schemaInstalls); diff --git a/meteor/jshint.sh b/meteor/jshint.sh new file mode 100755 index 0000000000..90653e8f5b --- /dev/null +++ b/meteor/jshint.sh @@ -0,0 +1,3 @@ +jshint client +jshint collections +jshint server diff --git a/meteor/lib/constants.js b/meteor/lib/constants.js new file mode 100644 index 0000000000..b53ada417d --- /dev/null +++ b/meteor/lib/constants.js @@ -0,0 +1,9 @@ +COMMON_WEB_PORTS = [ + 80, + 8000, + 8080, + 3000, + 5000, + 2368, + 1337 +] diff --git a/meteor/packages.json b/meteor/packages.json new file mode 100755 index 0000000000..8202aafb4e --- /dev/null +++ b/meteor/packages.json @@ -0,0 +1,7 @@ +{ + "dockerode": "https://github.com/usekite/dockerode/archive/7d05d8adeab8bf52c68e233814e7271d6f920cde.tar.gz", + "tar": "0.1.20", + "ansi-to-html": "0.2.0", + "async": "0.9.0", + "chokidar": "https://github.com/usekite/chokidar/archive/c5fd1c41102b2db4f9c5f1c74d38fd7076bbeb61.tar.gz" +} diff --git a/meteor/packages/.gitignore b/meteor/packages/.gitignore new file mode 100755 index 0000000000..2116d8628e --- /dev/null +++ b/meteor/packages/.gitignore @@ -0,0 +1,21 @@ +/bootstrap3-less +/iron-router +/npm +/blaze-layout +/headers +/inject-initial +/handlebar-helpers +/server-deps +/collection2 +/simple-schema +/collection-hooks +/moment +/underscore-string-latest +/blocking +/collection-helpers +/octicons +/fast-render +/iron-layout +/iron-core +/iron-dynamic-template +/iron-router-ga diff --git a/meteor/public/continue.png b/meteor/public/continue.png new file mode 100644 index 0000000000..368081310e Binary files /dev/null and b/meteor/public/continue.png differ diff --git a/meteor/public/continue@2x.png b/meteor/public/continue@2x.png new file mode 100644 index 0000000000..871299e886 Binary files /dev/null and b/meteor/public/continue@2x.png differ diff --git a/meteor/public/favicon.png b/meteor/public/favicon.png new file mode 100755 index 0000000000..5bded7cab3 Binary files /dev/null and b/meteor/public/favicon.png differ diff --git a/meteor/public/fonts/FontAwesome.otf b/meteor/public/fonts/FontAwesome.otf new file mode 100644 index 0000000000..3461e3fce6 Binary files /dev/null and b/meteor/public/fonts/FontAwesome.otf differ diff --git a/meteor/public/fonts/fontawesome-webfont.eot b/meteor/public/fonts/fontawesome-webfont.eot new file mode 100755 index 0000000000..6cfd566095 Binary files /dev/null and b/meteor/public/fonts/fontawesome-webfont.eot differ diff --git a/meteor/public/fonts/fontawesome-webfont.svg b/meteor/public/fonts/fontawesome-webfont.svg new file mode 100755 index 0000000000..a9f8469503 --- /dev/null +++ b/meteor/public/fonts/fontawesome-webfont.svg @@ -0,0 +1,504 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/meteor/public/fonts/fontawesome-webfont.ttf b/meteor/public/fonts/fontawesome-webfont.ttf new file mode 100755 index 0000000000..5cd6cff6d6 Binary files /dev/null and b/meteor/public/fonts/fontawesome-webfont.ttf differ diff --git a/meteor/public/fonts/fontawesome-webfont.woff b/meteor/public/fonts/fontawesome-webfont.woff new file mode 100755 index 0000000000..9eaecb3799 Binary files /dev/null and b/meteor/public/fonts/fontawesome-webfont.woff differ diff --git a/meteor/public/fonts/typicons.eot b/meteor/public/fonts/typicons.eot new file mode 100755 index 0000000000..0873b1208b Binary files /dev/null and b/meteor/public/fonts/typicons.eot differ diff --git a/meteor/public/fonts/typicons.svg b/meteor/public/fonts/typicons.svg new file mode 100755 index 0000000000..63929fe9bb --- /dev/null +++ b/meteor/public/fonts/typicons.svg @@ -0,0 +1,1180 @@ + + + + +Created by FontForge 20120731 at Sun Jul 27 14:53:18 2014 + By Stephen Hutchings +(c) Stephen Hutchings 2012 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/meteor/public/fonts/typicons.ttf b/meteor/public/fonts/typicons.ttf new file mode 100755 index 0000000000..f7df94bc77 Binary files /dev/null and b/meteor/public/fonts/typicons.ttf differ diff --git a/meteor/public/fonts/typicons.woff b/meteor/public/fonts/typicons.woff new file mode 100755 index 0000000000..14dc020627 Binary files /dev/null and b/meteor/public/fonts/typicons.woff differ diff --git a/meteor/public/image-placeholder.png b/meteor/public/image-placeholder.png new file mode 100755 index 0000000000..db27c421c4 Binary files /dev/null and b/meteor/public/image-placeholder.png differ diff --git a/meteor/public/install_logo.png b/meteor/public/install_logo.png new file mode 100644 index 0000000000..d457917193 Binary files /dev/null and b/meteor/public/install_logo.png differ diff --git a/meteor/public/install_logo@2x.png b/meteor/public/install_logo@2x.png new file mode 100644 index 0000000000..fe5daf0184 Binary files /dev/null and b/meteor/public/install_logo@2x.png differ diff --git a/meteor/public/logo-square-transparent.png b/meteor/public/logo-square-transparent.png new file mode 100755 index 0000000000..db32cf5b57 Binary files /dev/null and b/meteor/public/logo-square-transparent.png differ diff --git a/meteor/public/logo-square-white.png b/meteor/public/logo-square-white.png new file mode 100755 index 0000000000..936f098ae3 Binary files /dev/null and b/meteor/public/logo-square-white.png differ diff --git a/meteor/public/mac/logo.png b/meteor/public/mac/logo.png new file mode 100644 index 0000000000..708abc60cc Binary files /dev/null and b/meteor/public/mac/logo.png differ diff --git a/meteor/public/mac/logo@2x.png b/meteor/public/mac/logo@2x.png new file mode 100644 index 0000000000..32d9067824 Binary files /dev/null and b/meteor/public/mac/logo@2x.png differ diff --git a/meteor/public/mac/mac-close-hover.png b/meteor/public/mac/mac-close-hover.png new file mode 100644 index 0000000000..23f8a893ff Binary files /dev/null and b/meteor/public/mac/mac-close-hover.png differ diff --git a/meteor/public/mac/mac-close.png b/meteor/public/mac/mac-close.png new file mode 100644 index 0000000000..fcde02d4e6 Binary files /dev/null and b/meteor/public/mac/mac-close.png differ diff --git a/meteor/public/mac/mac-maximize-hover.png b/meteor/public/mac/mac-maximize-hover.png new file mode 100644 index 0000000000..724a786727 Binary files /dev/null and b/meteor/public/mac/mac-maximize-hover.png differ diff --git a/meteor/public/mac/mac-maximize.png b/meteor/public/mac/mac-maximize.png new file mode 100644 index 0000000000..8e11c52a23 Binary files /dev/null and b/meteor/public/mac/mac-maximize.png differ diff --git a/meteor/public/mac/mac-minimize-hover.png b/meteor/public/mac/mac-minimize-hover.png new file mode 100644 index 0000000000..8b6ac87746 Binary files /dev/null and b/meteor/public/mac/mac-minimize-hover.png differ diff --git a/meteor/public/mac/mac-minimize.png b/meteor/public/mac/mac-minimize.png new file mode 100644 index 0000000000..8e20822d3a Binary files /dev/null and b/meteor/public/mac/mac-minimize.png differ diff --git a/meteor/public/step_finished.png b/meteor/public/step_finished.png new file mode 100644 index 0000000000..c4283ff45a Binary files /dev/null and b/meteor/public/step_finished.png differ diff --git a/meteor/public/step_finished@2x.png b/meteor/public/step_finished@2x.png new file mode 100644 index 0000000000..9a852b8c14 Binary files /dev/null and b/meteor/public/step_finished@2x.png differ diff --git a/meteor/public/step_pending.png b/meteor/public/step_pending.png new file mode 100644 index 0000000000..8a04480087 Binary files /dev/null and b/meteor/public/step_pending.png differ diff --git a/meteor/public/step_pending@2x.png b/meteor/public/step_pending@2x.png new file mode 100644 index 0000000000..09bf04b30b Binary files /dev/null and b/meteor/public/step_pending@2x.png differ diff --git a/meteor/server/apps.js b/meteor/server/apps.js new file mode 100755 index 0000000000..1cd94115d8 --- /dev/null +++ b/meteor/server/apps.js @@ -0,0 +1,213 @@ +var watchers = {}; + +removeBindFolder = function (name, callback) { + exec(path.join(getBinDir(), 'boot2docker') + ' ssh "rm -rf /var/lib/docker/binds/' + name + '"', function(err, stdout) { + callback(err, stdout); + }); +}; + +removeAppWatcher = function (id) { + if (watchers[id]) { + watchers[id].watcher.close(); + delete watchers[id]; + } +}; + +addAppWatcher = function (app) { + removeAppWatcher(app._id); + var appPath = path.join(KITE_PATH, app.name); + var vmDir = path.join('/var/lib/docker/binds', app.name); + var vmPath = 'ssh://docker@localhost:2022/' + vmDir; + var watcher = chokidar.watch(appPath, {ignored: /[\/\\]\./}); + + var syncFunc = function () { + // Make sure that if they delete the app_name folder under ~/Kitematic, we don't delete all the volumes. + // Deleting files inside the app_name folder _will_ delete the volumes. + var rootMissing = ''; + if (!fs.existsSync(appPath)) { + rootMissing = '-ignorearchives'; + console.log('Created Kite ' + app.name + ' directory.'); + fs.mkdirSync(appPath, function (err) { + if (err) { throw err; } + }); + } + + var errorPattern = /The\sfile\s(.*)\son\shost/g; + var archiveErrorPattern = /Archive\s(.*)\son\shost\s.*\sshould\sbe\sDELETED/g; + var command = path.join(getBinDir(), 'unison') + ' ' + vmPath + ' ' + appPath + ' -prefer ' + vmPath + ' ' + rootMissing + ' -servercmd "sudo unison" -batch -log=false -confirmbigdel=false -ignore "Name {*.tmp,*.unison,*.swp,*.pyc,.DS_STORE}" -auto -sshargs "-o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no -o PreferredAuthentications=publickey -i ' + path.join(getHomePath(), '.ssh/id_boot2docker') + '"'; + exec(command, function (err) { + if (err) { + var results; + var location; + console.error(err); + try { + if (err.message.indexOf('the archives are locked.') !== -1) { + results = errorPattern.exec(err.message); + location = results[1].replace(' ', '\\ '); + exec('/bin/rm -rf ' + location, function () { + console.log('Removed unison file.'); + console.log(location); + }); + } + if (err.message.indexOf('The archive file is missing on some hosts') !== -1) { + results = archiveErrorPattern.exec(err.message); + location = results[1].replace(' ', '\\ '); + var fullLocation = path.join(getHomePath(), 'Library/Application\\ Support/Unison', location); + var cmd = '/bin/rm -rf ' + fullLocation; + exec(cmd, function () {}); + } + } catch (e) { + console.error(e); + } + } + }); + }; + + watchers[app._id] = { + watcher: watcher, + sync: syncFunc + }; + watcher.on('all', syncFunc); +}; + +resolveWatchers = function (callback) { + var apps = Apps.find({}).fetch(); + var ids = _.map(apps, function(app) { + return app._id; + }); + var watcherKeys = _.keys(watchers); + var toAdd = _.difference(ids, watcherKeys); + var toRemove = _.difference(watcherKeys, ids); + + _.each(toAdd, function (id) { + addAppWatcher(Apps.findOne(id), function () {}); + }); + + _.each(toRemove, function (id) { + removeAppWatcher(id); + }); + + // Run a sync for 'pulling' changes in the volumes. + _.each(watchers, function (watcher) { + watcher.sync(); + }); + + callback(); +}; + +recoverApps = function (callback) { + var apps = Apps.find({}).fetch(); + _.each(apps, function (app) { + // Update the app with the latest container info + if (!app.docker) { + return; + } + var container = docker.getContainer(app.docker.Id); + container.inspect(function (err, data) { + if (app.status !== 'STARTING' && !data.State.Running) { + console.log('restarting: ' + app.name); + console.log(app.docker.Id); + Fiber(function () { + restartApp(app, function (err) { + if (err) { console.error(err); } + }); + }).run(); + } + }); + }); + callback(); +}; + +Meteor.methods({ + recoverApps: function () { + return Meteor._wrapAsync(recoverApps)(); + }, + configVar: function (appId, configVars) { + this.unblock(); + Apps.update(appId, {$set: { + config: configVars, + status: 'STARTING' + }}); + var app = Apps.findOne({_id: appId}); + Meteor.call('runApp', app, function (err) { + if (err) { console.error(err); } + }); + }, + deleteApp: function (appId) { + this.unblock(); + var app = Apps.findOne(appId); + if (!app) { + throw new Meteor.Error(403, 'No app found with this ID'); + } + deleteApp(app, function (err) { + if (err) { console.error(err); } + var appPath = path.join(KITE_PATH, app.name); + deleteFolder(appPath); + removeAppWatcher(app._id); + removeBindFolder(app.name, function () { + console.log('Deleted Kite ' + app.name + ' directory.'); + Fiber(function () { + Apps.remove({_id: app._id}); + }).run(); + }); + }); + }, + createApp: function (formData) { + var validationResult = formValidate(formData, FormSchema.formCreateApp); + if (validationResult.errors) { + throw new Meteor.Error(400, 'Validation Failed.', validationResult.errors); + } else { + var cleaned = validationResult.cleaned; + var appObj = { + name: cleaned.name, + imageId: cleaned.imageId, + status: 'STARTING', + config: {} + }; + var appId = Apps.insert(appObj); + var appPath = path.join(KITE_PATH, appObj.name); + if (!fs.existsSync(appPath)) { + console.log('Created Kite ' + appObj.name + ' directory.'); + fs.mkdirSync(appPath, function (err) { + if (err) { throw err; } + }); + } + Apps.update(appId, { + $set: { + 'config.APP_ID': appId, + path: appPath + } + }); + var image = Images.findOne(appObj.imageId); + loadKiteVolumes(image.path, appObj.name); + var app = Apps.findOne(appId); + Meteor.call('runApp', app, function (err) { + if (err) { throw err; } + }); + } + }, + getAppLogs: function (appId) { + this.unblock(); + var app = Apps.findOne(appId); + if (app) { + getAppLogs(app, function (err) { + if (err) { throw err; } + }); + } + }, + restartApp: function (appId) { + this.unblock(); + var app = Apps.findOne(appId); + if (app && app.docker) { + Apps.update(app._id, {$set: { + status: 'STARTING' + }}); + restartApp(app, function (err) { + if (err) { console.error(err); } + }); + } + }, + resolveWatchers: function () { + return Meteor._wrapAsync(resolveWatchers)(); + } +}); diff --git a/meteor/server/docker.js b/meteor/server/docker.js new file mode 100755 index 0000000000..2387049e92 --- /dev/null +++ b/meteor/server/docker.js @@ -0,0 +1,631 @@ +Docker = Meteor.require('dockerode'); + +var Convert = Meteor.require('ansi-to-html'); +var convert = new Convert(); + +var DOCKER_HOST='192.168.59.103'; +docker = new Docker({host: '192.168.59.103', port: '2375'}); + +hasDockerfile = function (directory) { + return fs.existsSync(path.join(directory, 'Dockerfile')); +}; + +removeContainer = function (containerId, callback) { + var container = docker.getContainer(containerId); + container.kill(function (err) { + if (err) { callback(err); return; } + container.remove({v:1}, function (err) { + if (err) { callback(err); return; } + console.log('Deleted container: ' + containerId); + callback(null); + }); + }); +}; + +removeContainerSync = function (containerId) { + return Meteor._wrapAsync(removeContainer)(containerId); +}; + +deleteApp = function (app, callback) { + if (!app.docker) { + callback(null); + return; + } + try { + removeContainerSync(app.docker.Id); + } catch (e) { + console.error(e); + } + callback(null); +}; + +deleteAppSync = function (app) { + return Meteor._wrapAsync(deleteApp)(app); +}; + +getContainerData = function (containerId, callback) { + var container = docker.getContainer(containerId); + container.inspect(function (err, data) { + if (err) { + callback(err, null); + return; + } else { + data.Config.Volumes = convertVolumeObjToArray(data.Config.Volumes); + data.Volumes = convertVolumeObjToArray(data.Volumes); + data.VolumesRW = convertVolumeObjToArray(data.VolumesRW); + callback(null, data); + return; + } + }); +}; + +getContainerDataSync = function (containerId) { + return Meteor._wrapAsync(getContainerData)(containerId); +}; + +runContainer = function (app, image, callback) { + var envParam = []; + _.each(_.keys(app.config), function (key) { + var builtStr = key + '=' + app.config[key]; + envParam.push(builtStr); + }); + console.log(envParam); + docker.createContainer({ + Image: image._id.toLowerCase(), + Tty: false, + Env: envParam, + Hostname: app.name, + name: app.name + }, function (err, container) { + if (err) { callback(err, null); return; } + console.log('Created container: ' + container.id); + // Bind volumes + var binds = []; + if (image.docker.Config.Volumes.length > 0) { + _.each(image.docker.Config.Volumes, function (vol) { + binds.push('/var/lib/docker/binds/' + app.name + vol.Path + ':' + vol.Path); + }); + } + // Start the container + container.start({ + PublishAllPorts: true, + Binds: binds + }, function (err) { + if (err) { callback(err, null); return; } + console.log('Started container: ' + container.id); + callback(null, container); + }); + }); +}; + +runContainerSync = function (app, image) { + return Meteor._wrapAsync(runContainer)(app, image); +}; + +restartContainer = function (containerId, callback) { + var container = docker.getContainer(containerId); + container.restart(function (err) { + if (err) { + console.log(err); + callback(err); + return; + } + console.log('Restarted container: ' + containerId); + callback(null); + }); +}; + +restartContainerSync = function (containerId) { + return Meteor._wrapAsync(restartContainer)(containerId); +}; + +var getFromImage = function (dockerfile) { + var patternString = "(FROM)(.*)"; + var regex = new RegExp(patternString, "g"); + var fromInstruction = dockerfile.match(regex); + if (fromInstruction && fromInstruction.length > 0) { + return fromInstruction[0].split(' ')[1].trim(); + } else { + return null; + } +}; + +restartApp = function (app, callback) { + if (app.docker && app.docker.Id) { + restartContainerSync(app.docker.Id); + var containerData = getContainerDataSync(app.docker.Id); + Fiber(function () { + Apps.update(app._id, {$set: { + status: 'READY', + docker: containerData + }}); + }).run(); + callback(null); + } else { + callback(null); + } +}; + +getAppLogs = function (app) { + if (app.docker && app.docker.Id) { + var container = docker.getContainer(app.docker.Id); + container.logs({follow: false, stdout: true, stderr: true, timestamps: true, tail: 300}, function (err, response) { + if (err) { throw err; } + Fiber(function () { + Apps.update(app._id, { + $set: { + logs: [] + } + }); + }).run(); + var logs = []; + response.setEncoding('utf8'); + response.on('data', function (line) { + logs.push(convert.toHtml(line.slice(8))); + Fiber(function () { + Apps.update(app._id, { + $set: { + logs: logs + } + }); + }).run(); + }); + response.on('end', function () {}); + }); + } +}; + +createTarFile = function (image, callback) { + var TAR_PATH = path.join(KITE_TAR_PATH, image._id + '.tar'); + exec('tar czf ' + TAR_PATH + ' -C ' + image.path + ' .', function (err) { + if (err) { callback(err, null); return; } + console.log('Created tar file: ' + TAR_PATH); + callback(null, TAR_PATH); + }); +}; + +createTarFileSync = function (image) { + return Meteor._wrapAsync(createTarFile)(image); +}; + +var convertVolumeObjToArray = function (obj) { + var result = []; + _.each(_.keys(obj), function (key) { + var volumeObj = {}; + volumeObj.Path = key; + volumeObj.Value = obj[key]; + result.push(volumeObj); + }); + return result; +}; + +getImageData = function (imageId, callback) { + var image = docker.getImage(imageId.toLowerCase()); + image.inspect(function (err, data) { + if (err) { + callback(err, null); + return; + } else { + data.Config.Volumes = convertVolumeObjToArray(data.Config.Volumes); + data.ContainerConfig.Volumes = convertVolumeObjToArray(data.ContainerConfig.Volumes); + callback(null, data); + return; + } + }); +}; + +getImageDataSync = function (imageId) { + return Meteor._wrapAsync(getImageData)(imageId); +}; + +removeImage = function (imageId, callback) { + var image = docker.getImage(imageId.toLowerCase()); + image.remove({force: true}, function (err) { + if (err) { callback(err); return; } + console.log('Deleted image: ' + imageId); + callback(null); + }); +}; + +removeImageSync = function (imageId) { + return Meteor._wrapAsync(removeImage)(imageId); +}; + +deleteImage = function (image, callback) { + if (!image.docker) { + callback(null, {}); + return; + } + try { + removeImageSync(image.docker.Id); + } catch (e) { + console.error(e); + } + callback(null); +}; + +deleteImageSync = function (image) { + return Meteor._wrapAsync(deleteImage)(image); +}; + +var defaultContainerOptions = function () { + return [ + { + Image: 'kite-dns', + name: 'kite-dns', + PortBindings: {'53/udp': [{ 'HostPort': '53', 'HostIp': '172.17.42.1' }]}, + Binds: ['/var/run/docker.sock:/tmp/docker.sock'] + } + ]; +}; + +checkDefaultImages = function (callback) { + var defaultNames = defaultContainerOptions().map(function (container) { + return container.name; + }); + async.each(defaultNames, function (name, innerCallback) { + var image = docker.getImage(name); + image.inspect(function (err) { + if (err) { + if (err.reason === 'no such image') { + innerCallback('no such image'); + } else { + innerCallback(err); + } + } else { + innerCallback(); + } + }); + }, function (err) { + if (err) { + callback(err); + } else { + callback(); + } + }); +}; + +resolveDefaultImages = function () { + var defaultNames = defaultContainerOptions().map(function (container) { + return container.name; + }); + async.each(defaultNames, function (name, innerCallback) { + var image = docker.getImage(name); + image.inspect(function (err) { + if (err) { + if (err.reason === 'no such image') { + docker.loadImage(path.join(getBinDir(), 'base-images.tar.gz'), {}, function (err) { + if (err) { + innerCallback(err); + return; + } else { + innerCallback(); + } + }); + } else { + innerCallback(err); + } + } else { + innerCallback(); + } + }); + }); +}; + +checkDefaultContainers = function(callback) { + var defaultNames = defaultContainerOptions().map(function (container) { + return container.name; + }); + async.each(defaultNames, function (name, innerCallback) { + var container = docker.getContainer(name); + container.inspect(function (err, data) { + if (err) { + innerCallback(err); + } else { + if (data && data.State && data.State.Running) { + innerCallback(null); + } else { + innerCallback('Not running'); + } + } + }); + }, function (err) { + if (err) { + callback(err); + } else { + callback(); + } + }); +}; + +resolveDefaultContainers = function (callback) { + var defaultNames = defaultContainerOptions().map(function (container) { + return container.name; + }); + killAndRemoveContainers(defaultNames, function (err) { + if (err) { + callback(err); + return; + } + upContainers(defaultContainerOptions(), function (err) { + callback(err); + }); + }); +}; + +reloadDefaultContainers = function (callback) { + console.log('Reloading default containers.'); + + var defaultNames = defaultContainerOptions().map(function (container) { + return container.name; + }); + console.log('Removing old Kitematic default containers.'); + killAndRemoveContainers(defaultNames, function (err) { + console.log('Removed old Kitematic default containers.'); + if (err) { + console.log('Removing old Kitematic default containers ERROR.'); + callback(err); + return; + } + console.log('Removing old Kitematic default images.'); + removeImages(defaultNames, function () { + console.log('Removed old Kitematic default images.'); + // if (err) { + // console.log('Removing old Kitematic default images ERROR.'); + // console.log(err); + // callback(err); + // return; + // } + console.log('Loading new Kitematic default images.'); + docker.loadImage(path.join(getBinDir(), 'base-images.tar.gz'), {}, function (err) { + if (err) { + callback(err); + return; + } + console.log('Starting new Kitematic default containers.'); + upContainers(defaultContainerOptions(), function (err) { + callback(err); + }); + }); + }); + }); +}; + +upContainers = function (optionsList, callback) { + var createDefaultContainer = function (options, innerCallback) { + docker.createContainer(options, function (err, container) { + if (err) { + innerCallback(err); + return; + } + container.start({ + PublishAllPorts: true, + PortBindings: options.PortBindings, + Binds: options.Binds + }, function (err) { + innerCallback(err); + }); + }); + }; + + async.each(optionsList, function (options, innerCallback) { + var container = docker.getContainer(options.name); + container.inspect(function (err, data) { + if (err) { + if (err.reason.indexOf('no such container') !== -1) { + createDefaultContainer(options, function (err) { + innerCallback(err); + }); + } else { + innerCallback(err); + } + } else { + if (data && !data.State.Running) { + container.start(function (err) { + innerCallback(err); + }); + } else { + innerCallback(); + } + } + }); + }, function (err) { + callback(err); + }); +}; + +removeImages = function (names, callback) { + async.each(names, function (name, innerCallback) { + var image = docker.getImage(name); + image.remove(function (err) { + if (err) { + console.log('remove image error'); + console.log(err); + if (err.reason === 'no such image') { + innerCallback(); + } else { + innerCallback(err); + } + } else { + innerCallback(); + } + }); + }, function (err) { + callback(err); + }); +}; + +killAndRemoveContainers = function (names, callback) { + async.each(names, function (name, innerCallback) { + var container = docker.getContainer(name); + container.inspect(function (err, data) { + if (err) { + innerCallback(); + return; + } + if (data.State.Running) { + // Kill it + container.kill(function (err) { + if (err) { + innerCallback(err); + } else { + // Remove it + container.remove(function (err) { + innerCallback(err); + }); + } + }); + } else { + container.remove(function (err) { + innerCallback(err); + }); + } + }); + }, function (err) { + callback(err); + }); +}; + +pullImageFromDockerfile = function (dockerfile, imageId, callback) { + var fromImage = getFromImage(dockerfile); + console.log('From image: ' + fromImage); + if (fromImage) { + Fiber(function () { + Images.update(imageId, { + $set: { + buildLogs: [] + } + }); + }).run(); + var logs = []; + docker.pull(fromImage, function (err, response) { + if (err) { callback(err); return; } + response.setEncoding('utf8'); + response.on('data', function (data) { + try { + var logData = JSON.parse(data); + var logDisplay = ''; + if (logData.id) { + logDisplay += logData.id + ' | '; + } + logDisplay += logData.status; + if (logData.progressDetail && logData.progressDetail.current && logData.progressDetail.total) { + logDisplay += ' - ' + Math.round(logData.progressDetail.current / logData.progressDetail.total * 100) + '%'; + } + logs.push(logDisplay); + Fiber(function () { + Images.update(imageId, { + $set: { + buildLogs: logs + } + }); + }).run(); + } catch (e) { + console.error(e); + } + }); + response.on('end', function () { + console.log('Finished pulling image: ' + fromImage); + callback(null); + }); + }); + } +}; + +buildImage = function (image, callback) { + Fiber(function () { + var tarFilePath = createTarFileSync(image); + Images.update(image._id, { + $set: { + buildLogs: [] + } + }); + docker.buildImage(tarFilePath, {t: image._id.toLowerCase()}, function (err, response) { + if (err) { callback(err); } + console.log('Building Docker image...'); + var logs = []; + response.setEncoding('utf8'); + response.on('data', function (data) { + try { + var line = JSON.parse(data).stream; + logs.push(convert.toHtml(line)); + Fiber(function () { + Images.update(image._id, { + $set: { + buildLogs: logs + } + }); + }).run(); + } catch (e) { + console.error(e); + } + }); + response.on('end', function () { + console.log('Finished building Docker image.'); + try { + fs.unlinkSync(tarFilePath); + console.log('Cleaned up tar file.'); + } catch (e) { + console.error(e); + } + Fiber(function () { + var imageData = getImageDataSync(image._id); + var oldImageId = null; + if (image.docker && image.docker.Id) { + oldImageId = image.docker.Id; + } + if (oldImageId && oldImageId !== imageData.Id) { + removeImageSync(oldImageId); + } + Images.update(image._id, { + $set: { + docker: imageData, + status: 'READY' + } + }); + }).run(); + callback(null); + }); + }); + }).run(); +}; + +Meteor.methods({ + runApp: function (app) { + this.unblock(); + var image = Images.findOne({_id: app.imageId}); + try { + removeContainerSync(app.name); + } catch (e) {} + try { + var container = runContainerSync(app, image); + var containerData = getContainerDataSync(container.id); + Meteor.setTimeout(function () { + Apps.update(app._id, {$set: { + docker: containerData, + status: 'READY' + }}); + }, 2500); + } catch (e) { + console.error(e); + } + }, + getDockerHost: function () { + return DOCKER_HOST; + }, + reloadDefaultContainers: function () { + return Meteor._wrapAsync(reloadDefaultContainers)(); + }, + checkDefaultImages: function () { + return Meteor._wrapAsync(checkDefaultImages)(); + }, + resolveDefaultImages: function () { + return Meteor._wrapAsync(resolveDefaultImages)(); + }, + checkDefaultContainers: function () { + return Meteor._wrapAsync(checkDefaultContainers)(); + }, + resolveDefaultContainers: function () { + return Meteor._wrapAsync(resolveDefaultContainers)(); + } +}); diff --git a/meteor/server/form-schemas.js b/meteor/server/form-schemas.js new file mode 100755 index 0000000000..d3092abe04 --- /dev/null +++ b/meteor/server/form-schemas.js @@ -0,0 +1,44 @@ +FormSchema = { + + formCreateApp: { + name: { + label: 'app name', + required: true, + transforms: ['clean', 'slugify'], + messages: { + 'uniqueAppName': "This app name is already being used." + }, + rules: { + uniqueAppName: true + } + }, + imageId: { + label: 'image ID', + required: true, + transforms: ['clean'], + messages: { + 'required': "Please pick an image.", + 'validImageId': "This image ID is invalid." + }, + rules: { + validImageId: true + } + } + } + +}; + +// Auto-subscribe forms +_.each(_.keys(FormSchema), function (schemaName) { + console.log('Subscribed form schema: ' + schemaName); + var method = {}; + method[schemaName] = function (formInput) { + var result = formValidate(formInput, FormSchema[schemaName]); + if (result.errors) { + throw new Meteor.Error(400, 'Validation Failed.', result.errors); + } else { + return result.cleaned; + } + }; + Meteor.methods(method); +}); diff --git a/meteor/server/images.js b/meteor/server/images.js new file mode 100755 index 0000000000..e7ab45ef39 --- /dev/null +++ b/meteor/server/images.js @@ -0,0 +1,147 @@ +getImageMetaData = function (directory) { + var kiteJSON = getImageJSON(directory); + if (kiteJSON) { + if (!kiteJSON.name) { + kiteJSON.name = _.last(directory.split(path.sep)); + } + } else { + kiteJSON = { + name: _.last(directory.split(path.sep)) + }; + } + return kiteJSON; +}; + +rebuildImage = function (image, callback) { + deleteFolder(image.path); + var imageMetaData = getImageMetaData(image.originPath); + if (imageMetaData.logo) { + Images.update(image._id, { + $set: { + logoPath: path.join(image.path, imageMetaData.logo) + } + }); + } else { + Images.update(image._id, { + $set: { + logoPath: null + } + }); + } + Images.update(image._id, { + $set: { + status: 'BUILDING', + meta: imageMetaData + } + }); + image = Images.findOne(image._id); + saveImageFolderSync(image.originPath, image._id); + pullImageFromDockerfile(fs.readFileSync(path.join(image.path, 'Dockerfile'), 'utf8'), image._id, function (err) { + if (err) { callback(err, null); return; } + buildImage(image, function (err) { + if (err) { console.error(err); } + callback(null, null); + }); + }); +}; + +rebuildImageSync = function (image) { + return Meteor._wrapAsync(rebuildImage)(image); +}; + +Meteor.methods({ + createImage: function (directory) { + this.unblock(); + var imageObj = { + status: 'BUILDING', + originPath: directory + }; + var imageMetaData = getImageMetaData(directory); + imageObj.meta = imageMetaData; + var imageId = Images.insert(imageObj); + var imagePath = path.join(KITE_IMAGES_PATH, imageId); + Images.update(imageId, { + $set: { + path: imagePath + } + }); + if (imageObj.meta.logo) { + Images.update(imageId, { + $set: { + logoPath: path.join(imagePath, imageObj.meta.logo) + } + }); + } + var image = Images.findOne(imageId); + saveImageFolderSync(directory, imageId); + console.log('Saved folder sync'); + pullImageFromDockerfile(fs.readFileSync(path.join(image.path, 'Dockerfile'), 'utf8'), imageId, function (err) { + if (err) { throw err; } + buildImage(image, function (err) { + if (err) { console.error(err); } + }); + }); + }, + rebuildImage: function (imageId) { + this.unblock(); + var image = Images.findOne(imageId); + if (!image) { + throw new Meteor.Error(403, "No image found with this ID."); + } + var apps = Apps.find({imageId: imageId}).fetch(); + if (apps.length > 0) { + _.each(apps, function (app) { + console.log('Updating app: ' + app.name); + deleteAppSync(app); + Apps.update(app._id, { + $set: { + 'docker.Id': null, + status: 'STARTING', + logs: [] + } + }); + }); + rebuildImageSync(image); + _.each(apps, function (app) { + app = Apps.findOne(app._id); + Meteor.call('runApp', app, function (err) { + if (err) { console.error(err); } + }); + }); + } else { + rebuildImageSync(image); + } + }, + changeDirectory: function (imageId, directory) { + this.unblock(); + var image = Images.findOne(imageId); + if (!image) { + throw new Meteor.Error(403, "No image found with this ID."); + } + Images.update(imageId, { + $set: { + originPath: directory + } + }); + }, + validateDirectory: function (directory) { + if (!hasDockerfile(directory)) { + throw new Meteor.Error(400, "Only directories with Dockerfiles are supported now."); + } + }, + deleteImage: function (imageId) { + this.unblock(); + var image = Images.findOne(imageId); + if (!image) { + throw new Meteor.Error(403, "No image found with this ID."); + } + var app = Apps.findOne({imageId: imageId}); + if (!app) { + deleteImageSync(image); + deleteFolder(image.path); + Images.remove({_id: image._id}); + } else { + throw new Meteor.Error(400, 'This image is currently being used by ' + app.name + "."); + } + } +}); diff --git a/meteor/server/kite.js b/meteor/server/kite.js new file mode 100755 index 0000000000..712635616a --- /dev/null +++ b/meteor/server/kite.js @@ -0,0 +1,59 @@ +KITE_PATH = path.join(getHomePath(), 'Kitematic'); +KITE_TAR_PATH = path.join(KITE_PATH, '.tar'); +KITE_IMAGES_PATH = path.join(KITE_PATH, '.images'); + +if (!fs.existsSync(KITE_PATH)) { + console.log('Created Kitematic directory.'); + fs.mkdirSync(KITE_PATH, function (err) { + if (err) { throw err; } + }); +} + +if (!fs.existsSync(KITE_TAR_PATH)) { + console.log('Created Kitematic .tar directory.'); + fs.mkdirSync(KITE_TAR_PATH, function (err) { + if (err) { throw err; } + }); +} + +if (!fs.existsSync(KITE_IMAGES_PATH)) { + console.log('Created Kitematic .images directory.'); + fs.mkdirSync(KITE_IMAGES_PATH, function (err) { + if (err) { throw err; } + }); +} + +getImageJSON = function (directory) { + var KITE_JSON_PATH = path.join(directory, 'image.json'); + if (fs.existsSync(KITE_JSON_PATH)) { + var data = fs.readFileSync(KITE_JSON_PATH, 'utf8'); + return JSON.parse(data); + } else { + return null; + } +}; + +loadKiteVolumes = function (directory, appName) { + var KITE_VOLUMES_PATH = path.join(directory, 'volumes'); + if (fs.existsSync(KITE_VOLUMES_PATH)) { + var destinationPath = path.join(KITE_PATH, appName); + copyFolder(KITE_VOLUMES_PATH, destinationPath); + console.log('Copied volumes for: ' + appName); + } +}; + +saveImageFolder = function (directory, imageId, callback) { + var destinationPath = path.join(KITE_IMAGES_PATH, imageId); + if (!fs.existsSync(destinationPath)) { + fs.mkdirSync(destinationPath, function (err) { + if (err) { callback(err); return; } + }); + copyFolder(directory, destinationPath); + console.log('Copied image folder for: ' + imageId); + callback(null); + } +}; + +saveImageFolderSync = function (directory, imageId) { + return Meteor._wrapAsync(saveImageFolder)(directory, imageId); +}; diff --git a/meteor/server/lib/requires.js b/meteor/server/lib/requires.js new file mode 100755 index 0000000000..e4cbeaec97 --- /dev/null +++ b/meteor/server/lib/requires.js @@ -0,0 +1,9 @@ +https = Meteor.require('https'); +tar = Meteor.require('tar'); +zlib = Meteor.require('zlib'); +fs = Meteor.require('fs'); +path = Meteor.require('path'); +exec = Meteor.require('child_process').exec; +async = Meteor.require('async'); +Fiber = Meteor.require('fibers'); +chokidar = Meteor.require('chokidar'); diff --git a/meteor/server/lib/startup.js b/meteor/server/lib/startup.js new file mode 100644 index 0000000000..512c2ffc32 --- /dev/null +++ b/meteor/server/lib/startup.js @@ -0,0 +1,3 @@ +Meteor.startup(function () { + console.log('Kitematic started.'); +}); diff --git a/meteor/server/lib/utilities.js b/meteor/server/lib/utilities.js new file mode 100755 index 0000000000..9747fc139a --- /dev/null +++ b/meteor/server/lib/utilities.js @@ -0,0 +1,53 @@ +getHomePath = function () { + return process.env[(process.platform === 'win32') ? 'USERPROFILE' : 'HOME']; +}; + +getBinDir = function () { + if (process.env.NODE_ENV === 'development') { + return path.join(path.join(process.env.PWD, '..'), 'resources'); + } else { + return path.join(path.join(process.cwd(), '../../..'), 'resources'); + } +}; + +deleteFolder = function (directory) { + if (fs.existsSync(directory)) { + fs.readdirSync(directory).forEach(function (file) { + var curDirectory = directory + '/' + file; + if (fs.lstatSync(curDirectory).isDirectory()) { + // Recurse + deleteFolder(curDirectory); + } else { + // Delete File + try { + fs.unlinkSync(curDirectory); + } catch (e) { + console.error(e); + } + } + }); + fs.rmdirSync(directory); + } +}; + +copyFolder = function (src, dest) { + var exists = fs.existsSync(src); + var stats = exists && fs.statSync(src); + var isDirectory = exists && stats.isDirectory(); + if (exists && isDirectory) { + try { + fs.mkdirSync(dest); + } catch (e) { + console.error(e); + } + fs.readdirSync(src).forEach(function (childItemName) { + copyFolder(path.join(src, childItemName), path.join(dest, childItemName)); + }); + } else { + try { + fs.linkSync(src, dest); + } catch (e) { + console.error(e); + } + } +}; diff --git a/meteor/server/lib/validations.js b/meteor/server/lib/validations.js new file mode 100755 index 0000000000..5821dc7d85 --- /dev/null +++ b/meteor/server/lib/validations.js @@ -0,0 +1,141 @@ +var Rules = { + minLength: function (inputValue, ruleValue) { + return inputValue.length >= ruleValue; + }, + uniqueAppName: function (inputValue, ruleValue) { + if (ruleValue) { + var existingApps = Apps.find({name: inputValue}).fetch(); + return existingApps.length === 0; + } + }, + validImageId: function (inputValue, ruleValue) { + if (ruleValue) { + var existingImage = Images.findOne(inputValue); + if (existingImage) { + return true; + } else { + return false; + } + } + } +}; + +var Transforms = { + clean: function (string) { + return _(string).clean(); + }, + capitalize: function (string) { + return _(string).capitalize(); + }, + slugify: function (string) { + return _(string).slugify(); + }, + toLowerCase: function (string) { + return string.toLowerCase(); + } +}; + +var Formats = { + letters: /^[a-zA-Z\ \']+$/, + alphanumeric: /^[a-zA-Z0-9\ \']+$/, + email: /^(([^<>()\[\]\\.,;:\s@\"]+(\.[^<>()\[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/, + phone: /^\b\d{3}[\-.]?\d{3}[\-.]?\d{4}\b$/ +}; + +var runTransforms = function (input, transforms) { + var result = input; + _.each(transforms, function (transform) { + result = Transforms[transform](result); + }); + return result; +}; + +var cleanForm = function (formInput, formSchema) { + var cleanedForm = {}; + for (var name in formInput) { + if (formSchema[name]) { + if (formSchema[name].transforms) { + cleanedForm[name] = runTransforms(formInput[name], formSchema[name].transforms); + } else { + cleanedForm[name] = formInput[name]; + } + } + } + return cleanedForm; +}; + +var buildMessage = function (errorName, schema) { + if (errorName === 'required') { + if (!schema.messages || !schema.messages.required) { + var prefix = 'a'; + if (_.contains(['a', 'e', 'i', 'o', 'u'], schema.label[0].toLowerCase())) { + prefix = 'an'; + } + return 'Please provide ' + prefix + ' ' + schema.label + '.'; + } + } + if (errorName === 'format') { + if (!schema.messages || !schema.messages.format) { + return 'Please provide a valid ' + schema.label + '.'; + } + } + return schema.messages[errorName].replace('{{label}}', schema.label); +}; + +var validFormat = function (input, schema) { + return Formats[schema.format].test(input); +}; + +var runRules = function (inputValue, schema, errorObj, cleanedFormInput) { + var rules = schema.rules; + if (rules) { + for (var name in rules) { + if (rules.hasOwnProperty(name)) { + var ruleValue = rules[name]; + if (!Rules[name](inputValue, ruleValue, cleanedFormInput)) { + errorObj[name] = buildMessage(name, schema); + } + } + } + } +}; + +formValidate = function (formInput, formSchema) { + var errorData = {}; + var cleanedFormInput = cleanForm(formInput, formSchema); + for (var name in cleanedFormInput) { + if (cleanedFormInput.hasOwnProperty(name)) { + var errorObj = {}; + var input = cleanedFormInput[name]; + var schema = formSchema[name]; + if (schema.required) { + if (!input) { + errorObj.required = buildMessage('required', schema); + } + } + if (input) { + if (schema.format) { + if (!validFormat(input, schema)) { + errorObj.format = buildMessage('format', schema); + } + } + if (schema.rules) { + runRules(input, schema, errorObj, cleanedFormInput); + } + } + if (Object.keys(errorObj).length > 0) { + errorData[name] = errorObj; + } + } + } + if (Object.keys(errorData).length > 0) { + return { + errors: errorData, + cleaned: cleanedFormInput + }; + } else { + return { + cleaned: cleanedFormInput + }; + } +}; diff --git a/meteor/server/publications.js b/meteor/server/publications.js new file mode 100755 index 0000000000..fe75a8a9ba --- /dev/null +++ b/meteor/server/publications.js @@ -0,0 +1,11 @@ +Meteor.publish('apps', function () { + return Apps.find({}, {sort: {createdAt: -1}}); +}); + +Meteor.publish('images', function () { + return Images.find({}, {sort: {createdAt: -1}}); +}); + +Meteor.publish('installs', function () { + return Installs.find({}, {sort: {createdAt: -1}}); +}); diff --git a/meteor/settings_dev.json b/meteor/settings_dev.json new file mode 100644 index 0000000000..19b3d63fd8 --- /dev/null +++ b/meteor/settings_dev.json @@ -0,0 +1,10 @@ +{ + "public": { + "ga": { + "id": "UA-53012639-2", + "create": { + "cookieDomain": "none" + } + } + } +} diff --git a/meteor/smart.json b/meteor/smart.json new file mode 100755 index 0000000000..6959af2f2d --- /dev/null +++ b/meteor/smart.json @@ -0,0 +1,17 @@ +{ + "packages": { + "bootstrap3-less": {}, + "iron-router": {}, + "npm": {}, + "headers": {}, + "handlebar-helpers": {}, + "collection2": {}, + "collection-hooks": {}, + "moment": {}, + "underscore-string-latest": {}, + "collection-helpers": {}, + "octicons": {}, + "fast-render": {}, + "iron-router-ga": {} + } +} diff --git a/meteor/smart.lock b/meteor/smart.lock new file mode 100755 index 0000000000..20d2835feb --- /dev/null +++ b/meteor/smart.lock @@ -0,0 +1,117 @@ +{ + "meteor": {}, + "dependencies": { + "basePackages": { + "bootstrap3-less": {}, + "iron-router": {}, + "npm": {}, + "headers": {}, + "handlebar-helpers": {}, + "collection2": {}, + "collection-hooks": {}, + "moment": {}, + "underscore-string-latest": {}, + "collection-helpers": {}, + "octicons": {}, + "fast-render": {}, + "iron-router-ga": {} + }, + "packages": { + "bootstrap3-less": { + "git": "https://github.com/simison/bootstrap3-less", + "tag": "v0.2.1", + "commit": "dc94a51ed00d7de6d6f2b6d65107a876d849ad61" + }, + "iron-router": { + "git": "https://github.com/EventedMind/iron-router.git", + "tag": "v0.8.2", + "commit": "05415a8891ea87a00fb1e2388585f2ca5a38e0da" + }, + "npm": { + "git": "https://github.com/arunoda/meteor-npm.git", + "tag": "v0.2.6", + "commit": "177ab6118de5bf8cffb19481343d5762ff7a2aaf" + }, + "headers": { + "git": "https://github.com/gadicohen/meteor-headers.git", + "tag": "v0.0.24", + "commit": "3c09e682895e13c71ca0114baf3c09ee9c507709" + }, + "handlebar-helpers": { + "git": "https://github.com/raix/Meteor-handlebar-helpers.git", + "tag": "v0.1.1", + "commit": "0b407ab65e7c1ebd53d71aef0de2e2c1d21a597c" + }, + "collection2": { + "git": "https://github.com/aldeed/meteor-collection2.git", + "tag": "v0.4.6", + "commit": "80554182486be0d8e74f7ed02194a5649d712e27" + }, + "collection-hooks": { + "git": "https://github.com/matb33/meteor-collection-hooks.git", + "tag": "v0.7.2", + "commit": "261f61f07b371ae913463fba8964a9b93cab531b" + }, + "moment": { + "git": "https://github.com/acreeger/meteor-moment.git", + "tag": "v2.8.1", + "commit": "722ea63783d594341023836b7d418ab2567dab8c" + }, + "underscore-string-latest": { + "git": "https://github.com/TimHeckel/meteor-underscore-string.git", + "tag": "v2.3.3", + "commit": "4a5d70eee48fbd90a6e6fc78747250d704a0b3bb" + }, + "collection-helpers": { + "git": "https://github.com/dburles/meteor-collection-helpers.git", + "tag": "v0.3.1", + "commit": "eff6c859cd91eae324f6c99ab755992d0f271d91" + }, + "octicons": { + "git": "https://github.com/Keith-S/meteor-octicons.git", + "tag": "v0.1.0", + "commit": "6bb85a8b4e7d8a23ccbfb8d5c043431a135301b4" + }, + "fast-render": { + "git": "https://github.com/arunoda/meteor-fast-render.git", + "tag": "v1.0.0", + "commit": "acbc04982025fe78cebb8865b5a04689741d4b0b" + }, + "iron-router-ga": { + "git": "https://github.com/reywood/meteor-iron-router-ga.git", + "tag": "v0.2.5", + "commit": "ede54c4633f9a54fddd5c431202a88284df2a932" + }, + "iron-layout": { + "git": "https://github.com/EventedMind/iron-layout.git", + "tag": "v0.2.0", + "commit": "4a2d53e35ba036b0c189c7ceca34be494d4c6c97" + }, + "inject-initial": { + "git": "https://github.com/gadicc/meteor-inject-initial.git", + "tag": "v0.0.8", + "commit": "90f2fbcc5b4bc17fa4d4535f47813e31d86033b4" + }, + "simple-schema": { + "git": "https://github.com/aldeed/meteor-simple-schema.git", + "tag": "v0.7.0", + "commit": "77d267aec4ba8a70f677e5d9ef9fb91fb0e3f0f6" + }, + "blaze-layout": { + "git": "https://github.com/EventedMind/blaze-layout.git", + "tag": "v0.2.5", + "commit": "273e3ab7d005d91a1a59c71bd224533b4dae2fbd" + }, + "iron-core": { + "git": "https://github.com/EventedMind/iron-core.git", + "tag": "v0.2.0", + "commit": "0e48b5dc50d03f01025b7b900fb5ce2f13d52cad" + }, + "iron-dynamic-template": { + "git": "https://github.com/EventedMind/iron-dynamic-template.git", + "tag": "v0.2.1", + "commit": "4dd1185c4d9d616c9abdb3f33e4a7d5a88db7e18" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000000..e497ac1370 --- /dev/null +++ b/package.json @@ -0,0 +1,26 @@ +{ + "name": "Kitematic", + "main": "index.html", + "node-remote": "", + "version": "0.1.0", + "window": { + "show": false, + "toolbar": false, + "frame": false, + "width": 800, + "height": 600, + "resizable": false + }, + "engines": { + "node": "0.11.13" + }, + "devDependencies": { + "grunt-shell": "^0.7.0" + }, + "dependencies": { + "async": "^0.9.0", + "exec": "^0.1.2", + "moment": "2.8.1", + "open": "0.0.5" + } +} diff --git a/resources/MONGOD_LICENSE.txt b/resources/MONGOD_LICENSE.txt new file mode 100644 index 0000000000..dba13ed2dd --- /dev/null +++ b/resources/MONGOD_LICENSE.txt @@ -0,0 +1,661 @@ + GNU AFFERO GENERAL PUBLIC LICENSE + Version 3, 19 November 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU Affero General Public License is a free, copyleft license for +software and other kinds of works, specifically designed to ensure +cooperation with the community in the case of network server software. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +our General Public Licenses are intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + Developers that use our General Public Licenses protect your rights +with two steps: (1) assert copyright on the software, and (2) offer +you this License which gives you legal permission to copy, distribute +and/or modify the software. + + A secondary benefit of defending all users' freedom is that +improvements made in alternate versions of the program, if they +receive widespread use, become available for other developers to +incorporate. Many developers of free software are heartened and +encouraged by the resulting cooperation. However, in the case of +software used on network servers, this result may fail to come about. +The GNU General Public License permits making a modified version and +letting the public access it on a server without ever releasing its +source code to the public. + + The GNU Affero General Public License is designed specifically to +ensure that, in such cases, the modified source code becomes available +to the community. It requires the operator of a network server to +provide the source code of the modified version running there to the +users of that server. Therefore, public use of a modified version, on +a publicly accessible server, gives the public access to the source +code of the modified version. + + An older license, called the Affero General Public License and +published by Affero, was designed to accomplish similar goals. This is +a different license, not a version of the Affero GPL, but Affero has +released a new version of the Affero GPL which permits relicensing under +this license. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU Affero General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Remote Network Interaction; Use with the GNU General Public License. + + Notwithstanding any other provision of this License, if you modify the +Program, your modified version must prominently offer all users +interacting with it remotely through a computer network (if your version +supports such interaction) an opportunity to receive the Corresponding +Source of your version by providing access to the Corresponding Source +from a network server at no charge, through some standard or customary +means of facilitating copying of software. This Corresponding Source +shall include the Corresponding Source for any work covered by version 3 +of the GNU General Public License that is incorporated pursuant to the +following paragraph. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the work with which it is combined will remain governed by version +3 of the GNU General Public License. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU Affero General Public License from time to time. Such new versions +will be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU Affero General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU Affero General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU Affero General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If your software can interact with users remotely through a computer +network, you should also make sure that it provides a way for users to +get its source. For example, if your program is a web application, its +interface could display a "Source" link that leads users to an archive +of the code. There are many ways you could offer source, and different +solutions will be better for different programs; see section 13 for the +specific requirements. + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU AGPL, see +. diff --git a/resources/UNISON_LICENSE.txt b/resources/UNISON_LICENSE.txt new file mode 100644 index 0000000000..356d8987c9 --- /dev/null +++ b/resources/UNISON_LICENSE.txt @@ -0,0 +1,682 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + + +ADDITIONAL TERMS APPLICABLE TO THE Doom 3 BFG Edition GPL Source Code. + + The following additional terms ("Additional Terms") supplement and modify + the GNU General Public License, Version 3 ("GPL") applicable to the Doom 3 + BFG Edition GPL Source Code ("Doom 3 BFG Edition Source Code"). In addition + to the terms and conditions of the GPL, the Doom 3 BFG Edition Source Code is + subject to the further restrictions below. + +1. Replacement of Section 15. Section 15 of the GPL shall be deleted in its +entirety and replaced with the following: + +"15. Disclaimer of Warranty. + +THE PROGRAM IS PROVIDED WITHOUT ANY WARRANTIES, WHETHER EXPRESSED OR IMPLIED, +INCLUDING, WITHOUT LIMITATION, IMPLIED WARRANTIES OF FITNESS FOR A PARTICULAR +PURPOSE, NON-INFRINGEMENT, TITLE AND MERCHANTABILITY. THE PROGRAM IS BEING +DELIVERED OR MADE AVAILABLE "AS IS", "WITH ALL FAULTS" AND WITHOUT WARRANTY OR +REPRESENTATION. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE +PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST +OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION." + +2. Replacement of Section 16. Section 16 of the GPL shall be deleted in its +entirety and replaced with the following: + +"16. LIMITATION OF LIABILITY. + +UNDER NO CIRCUMSTANCES SHALL ANY COPYRIGHT HOLDER OR ITS AFFILIATES, OR ANY +OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE +LIABLE TO YOU, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, FOR ANY +DAMAGES OR OTHER LIABILITY, INCLUDING ANY GENERAL, DIRECT, INDIRECT, SPECIAL, +INCIDENTAL, CONSEQUENTIAL OR PUNITIVE DAMAGES ARISING FROM, OUT OF OR IN +CONNECTION WITH THE USE OR INABILITY TO USE THE PROGRAM OR OTHER DEALINGS WITH +THE PROGRAM(INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED +INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE +PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), WHETHER OR NOT ANY COPYRIGHT +HOLDER OR SUCH OTHER PARTY RECEIVES NOTICE OF ANY SUCH DAMAGES AND WHETHER +OR NOT SUCH DAMAGES COULD HAVE BEEN FORESEEN." + +3. LEGAL NOTICES; NO TRADEMARK LICENSE; ORIGIN. You must reproduce faithfully +all trademark, copyright and other proprietary and legal notices on any copies +of the Program or any other required author attributions. This license does +not grant you rights to use any copyright holder or any other party’s name, +logo, or trademarks. Neither the name of the copyright holder or its +affiliates, or any other party who modifies and/or conveys the Program may be +used to endorse or promote products derived from this software without +specific prior written permission. The origin of the Program must not be +misrepresented; you must not claim that you wrote the original Program. + +Altered source versions must be plainly marked as such, and must not be +misrepresented as being the original Program. + +4. INDEMNIFICATION. IF YOU CONVEY A COVERED WORK AND AGREE WITH ANY RECIPIENT +OF THAT COVERED WORK THAT YOU WILL ASSUME ANY LIABILITY FOR THAT COVERED WORK, +YOU HEREBY AGREE TO INDEMNIFY, DEFEND AND HOLD HARMLESS THE OTHER LICENSORS +AND AUTHORS OF THAT COVERED WORK FOR ANY DAMAEGS, DEMANDS, CLAIMS, LOSSES, +CAUSES OF ACTION, LAWSUITS, JUDGMENTS EXPENSES (INCLUDING WITHOUT LIMITATION +REASONABLE ATTORNEYS' FEES AND EXPENSES) OR ANY OTHER LIABLITY ARISING FROM, +RELATED TO OR IN CONNECTION WITH YOUR ASSUMPTIONS OF LIABILITY. \ No newline at end of file diff --git a/resources/cocoasudo b/resources/cocoasudo new file mode 100755 index 0000000000..ccf0bf8aa1 Binary files /dev/null and b/resources/cocoasudo differ diff --git a/resources/install b/resources/install new file mode 100755 index 0000000000..46b5a22bb1 --- /dev/null +++ b/resources/install @@ -0,0 +1,47 @@ +#!/bin/sh + +# Lookup the Kitematic VM resolver for .dev domains +echo "nameserver 172.17.42.1" > /etc/resolver/dev + +# Install virtualbox +if [ -n "$INSTALL_VIRTUALBOX" ]; then + /usr/sbin/installer -pkg $VIRTUALBOX_PKG_PATH -target / +fi + +/bin/rm -rf /Library/LaunchAgents/com.kitematic.route.plist + +echo ' + + + + Label + com.kitematic.route + ProgramArguments + + /sbin/route + -n + add + 172.17.0.0/16 + 192.168.59.103 + + KeepAlive + + RunAtLoad + + ServiceIPC + + UserName + root + LaunchOnlyOnce + + +' > /Library/LaunchAgents/com.kitematic.route.plist + +DIR=$(dirname "$0") +USER=`w -h | sort -u -t' ' -k1,1 | awk '{print $1}'` + +sudo -u $USER $DIR/boot2docker init + +# Add entries to routing table for Kitematic VM +/sbin/route delete 172.17.0.0/16 192.168.59.103 +/sbin/route -n add 172.17.0.0/16 192.168.59.103 \ No newline at end of file diff --git a/resources/kite-binaries.tar.gz b/resources/kite-binaries.tar.gz new file mode 100644 index 0000000000..43215c64bd Binary files /dev/null and b/resources/kite-binaries.tar.gz differ diff --git a/resources/kite-dns/Dockerfile b/resources/kite-dns/Dockerfile new file mode 100644 index 0000000000..eeb3cb87b2 --- /dev/null +++ b/resources/kite-dns/Dockerfile @@ -0,0 +1,18 @@ +FROM ubuntu:14.04 +MAINTAINER Kite + +RUN apt-get update +RUN apt-get install -q -y dnsmasq wget psmisc + +RUN mkdir /app +WORKDIR /app +RUN wget https://github.com/jwilder/docker-gen/releases/download/0.3.2/docker-gen-linux-amd64-0.3.2.tar.gz +RUN tar xvzf docker-gen-linux-amd64-0.3.2.tar.gz +RUN rm docker-gen-linux-amd64-0.3.2.tar.gz + +ADD . /app + +ENV DOCKER_HOST unix:///tmp/docker.sock + +EXPOSE 53/udp +CMD ["sh", "/app/start"] \ No newline at end of file diff --git a/resources/kite-dns/dnsmasq.tmpl b/resources/kite-dns/dnsmasq.tmpl new file mode 100644 index 0000000000..546fda7910 --- /dev/null +++ b/resources/kite-dns/dnsmasq.tmpl @@ -0,0 +1,6 @@ +user=root +{{ range $index, $value := $ }} + {{ with $address := index $value.Addresses 0 }} +address=/{{$value.Name}}.dev/{{$address.IP}} + {{ end }} +{{ end }} diff --git a/resources/kite-dns/restart b/resources/kite-dns/restart new file mode 100644 index 0000000000..cfd46a988c --- /dev/null +++ b/resources/kite-dns/restart @@ -0,0 +1,2 @@ +killall dnsmasq +dnsmasq \ No newline at end of file diff --git a/resources/kite-dns/start b/resources/kite-dns/start new file mode 100644 index 0000000000..66cbfcb602 --- /dev/null +++ b/resources/kite-dns/start @@ -0,0 +1 @@ +/app/docker-gen -watch -only-exposed -notify "sh /app/restart" /app/dnsmasq.tmpl /etc/dnsmasq.conf \ No newline at end of file diff --git a/resources/mongo-livedata.js b/resources/mongo-livedata.js new file mode 100644 index 0000000000..d4eca08c24 --- /dev/null +++ b/resources/mongo-livedata.js @@ -0,0 +1,4004 @@ +(function () { + +/* Imports */ +var Meteor = Package.meteor.Meteor; +var Random = Package.random.Random; +var EJSON = Package.ejson.EJSON; +var _ = Package.underscore._; +var LocalCollection = Package.minimongo.LocalCollection; +var Minimongo = Package.minimongo.Minimongo; +var Log = Package.logging.Log; +var DDP = Package.livedata.DDP; +var DDPServer = Package.livedata.DDPServer; +var Deps = Package.deps.Deps; +var AppConfig = Package['application-configuration'].AppConfig; +var check = Package.check.check; +var Match = Package.check.Match; +var MaxHeap = Package['binary-heap'].MaxHeap; +var MinMaxHeap = Package['binary-heap'].MinMaxHeap; +var Hook = Package['callback-hook'].Hook; +var TingoDB = Npm.require('tingodb')().Db; + +/* Package-scope variables */ +var MongoInternals, MongoTest, MongoConnection, CursorDescription, Cursor, listenAll, forEachTrigger, OPLOG_COLLECTION, idForOp, OplogHandle, ObserveMultiplexer, ObserveHandle, DocFetcher, PollingObserveDriver, OplogObserveDriver, LocalCollectionDriver; + +(function () { + +///////////////////////////////////////////////////////////////////////////////////////////////////////// +// // +// packages/mongo-livedata/mongo_driver.js // +// // +///////////////////////////////////////////////////////////////////////////////////////////////////////// + // +/** // 1 + * Provide a synchronous Collection API using fibers, backed by // 2 + * MongoDB. This is only for use on the server, and mostly identical // 3 + * to the client API. // 4 + * // 5 + * NOTE: the public API methods must be run within a fiber. If you call // 6 + * these outside of a fiber they will explode! // 7 + */ // 8 + // 9 +var path = Npm.require('path'); // 10 +var MongoDB = Npm.require('mongodb'); // 11 +var Fiber = Npm.require('fibers'); // 12 +var Future = Npm.require(path.join('fibers', 'future')); // 13 + // 14 +MongoInternals = {}; // 15 +MongoTest = {}; // 16 + // 17 +// This is used to add or remove EJSON from the beginning of everything nested // 18 +// inside an EJSON custom type. It should only be called on pure JSON! // 19 +var replaceNames = function (filter, thing) { // 20 + if (typeof thing === "object") { // 21 + if (_.isArray(thing)) { // 22 + return _.map(thing, _.bind(replaceNames, null, filter)); // 23 + } // 24 + var ret = {}; // 25 + _.each(thing, function (value, key) { // 26 + ret[filter(key)] = replaceNames(filter, value); // 27 + }); // 28 + return ret; // 29 + } // 30 + return thing; // 31 +}; // 32 + // 33 +// Ensure that EJSON.clone keeps a Timestamp as a Timestamp (instead of just // 34 +// doing a structural clone). // 35 +// XXX how ok is this? what if there are multiple copies of MongoDB loaded? // 36 +MongoDB.Timestamp.prototype.clone = function () { // 37 + // Timestamps should be immutable. // 38 + return this; // 39 +}; // 40 + // 41 +var makeMongoLegal = function (name) { return "EJSON" + name; }; // 42 +var unmakeMongoLegal = function (name) { return name.substr(5); }; // 43 + // 44 +var replaceMongoAtomWithMeteor = function (document) { // 45 + if (document instanceof MongoDB.Binary) { // 46 + var buffer = document.value(true); // 47 + return new Uint8Array(buffer); // 48 + } // 49 + if (document instanceof MongoDB.ObjectID) { // 50 + return new Meteor.Collection.ObjectID(document.toHexString()); // 51 + } // 52 + if (document["EJSON$type"] && document["EJSON$value"] // 53 + && _.size(document) === 2) { // 54 + return EJSON.fromJSONValue(replaceNames(unmakeMongoLegal, document)); // 55 + } // 56 + if (document instanceof MongoDB.Timestamp) { // 57 + // For now, the Meteor representation of a Mongo timestamp type (not a date! // 58 + // this is a weird internal thing used in the oplog!) is the same as the // 59 + // Mongo representation. We need to do this explicitly or else we would do a // 60 + // structural clone and lose the prototype. // 61 + return document; // 62 + } // 63 + return undefined; // 64 +}; // 65 + // 66 +var replaceMeteorAtomWithMongo = function (document) { // 67 + if (EJSON.isBinary(document)) { // 68 + // This does more copies than we'd like, but is necessary because // 69 + // MongoDB.BSON only looks like it takes a Uint8Array (and doesn't actually // 70 + // serialize it correctly). // 71 + return new MongoDB.Binary(new Buffer(document)); // 72 + } // 73 + if (document instanceof Meteor.Collection.ObjectID) { // 74 + return new MongoDB.ObjectID(document.toHexString()); // 75 + } // 76 + if (document instanceof MongoDB.Timestamp) { // 77 + // For now, the Meteor representation of a Mongo timestamp type (not a date! // 78 + // this is a weird internal thing used in the oplog!) is the same as the // 79 + // Mongo representation. We need to do this explicitly or else we would do a // 80 + // structural clone and lose the prototype. // 81 + return document; // 82 + } // 83 + if (EJSON._isCustomType(document)) { // 84 + return replaceNames(makeMongoLegal, EJSON.toJSONValue(document)); // 85 + } // 86 + // It is not ordinarily possible to stick dollar-sign keys into mongo // 87 + // so we don't bother checking for things that need escaping at this time. // 88 + return undefined; // 89 +}; // 90 + // 91 +var replaceTypes = function (document, atomTransformer) { // 92 + if (typeof document !== 'object' || document === null) // 93 + return document; // 94 + // 95 + var replacedTopLevelAtom = atomTransformer(document); // 96 + if (replacedTopLevelAtom !== undefined) // 97 + return replacedTopLevelAtom; // 98 + // 99 + var ret = document; // 100 + _.each(document, function (val, key) { // 101 + var valReplaced = replaceTypes(val, atomTransformer); // 102 + if (val !== valReplaced) { // 103 + // Lazy clone. Shallow copy. // 104 + if (ret === document) // 105 + ret = _.clone(document); // 106 + ret[key] = valReplaced; // 107 + } // 108 + }); // 109 + return ret; // 110 +}; // 111 + // 112 + // 113 +MongoConnection = function (url, options) { // 114 + var self = this; // 115 + options = options || {}; // 116 + self._connectCallbacks = []; // 117 + self._observeMultiplexers = {}; // 118 + self._onFailoverHook = new Hook; // 119 + // 120 + var mongoOptions = {db: {safe: true}, server: {}, replSet: {}}; // 121 + // 122 + // Set autoReconnect to true, unless passed on the URL. Why someone // 123 + // would want to set autoReconnect to false, I'm not really sure, but // 124 + // keeping this for backwards compatibility for now. // 125 + if (!(/[\?&]auto_?[rR]econnect=/.test(url))) { // 126 + mongoOptions.server.auto_reconnect = true; // 127 + } // 128 + // 129 + // Disable the native parser by default, unless specifically enabled // 130 + // in the mongo URL. // 131 + // - The native driver can cause errors which normally would be // 132 + // thrown, caught, and handled into segfaults that take down the // 133 + // whole app. // 134 + // - Binary modules don't yet work when you bundle and move the bundle // 135 + // to a different platform (aka deploy) // 136 + // We should revisit this after binary npm module support lands. // 137 + if (!(/[\?&]native_?[pP]arser=/.test(url))) { // 138 + mongoOptions.db.native_parser = false; // 139 + } // 140 + // 141 + // XXX maybe we should have a better way of allowing users to configure the // 142 + // underlying Mongo driver // 143 + if (_.has(options, 'poolSize')) { // 144 + // If we just set this for "server", replSet will override it. If we just // 145 + // set it for replSet, it will be ignored if we're not using a replSet. // 146 + mongoOptions.server.poolSize = options.poolSize; // 147 + mongoOptions.replSet.poolSize = options.poolSize; // 148 + } // 149 + + + var dbPath; + if (process.env.DB_PATH) { + dbPath = process.env.DB_PATH; + } else { + dbPath = process.cwd(); + } + + var db = new TingoDB(dbPath, {}); + self.db = db; + Fiber(function () { + _.each(self._connectCallbacks, function (c) { + c(db); + }); + }).run(); // 186 + // 187 + self._docFetcher = new DocFetcher(self); // 188 + self._oplogHandle = null; // 189 + // 190 + if (options.oplogUrl && !Package['disable-oplog']) { // 191 + var dbNameFuture = new Future; // 192 + self._withDb(function (db) { // 193 + dbNameFuture.return(db.databaseName); // 194 + }); // 195 + self._oplogHandle = new OplogHandle(options.oplogUrl, dbNameFuture.wait()); // 196 + } // 197 +}; // 198 + // 199 +MongoConnection.prototype.close = function() { // 200 + var self = this; // 201 + // 202 + // XXX probably untested // 203 + var oplogHandle = self._oplogHandle; // 204 + self._oplogHandle = null; // 205 + if (oplogHandle) // 206 + oplogHandle.stop(); // 207 + // 208 + // Use Future.wrap so that errors get thrown. This happens to // 209 + // work even outside a fiber since the 'close' method is not // 210 + // actually asynchronous. // 211 + Future.wrap(_.bind(self.db.close, self.db))(true).wait(); // 212 +}; // 213 + // 214 +MongoConnection.prototype._withDb = function (callback) { // 215 + var self = this; // 216 + if (self.db) { // 217 + callback(self.db); // 218 + } else { // 219 + self._connectCallbacks.push(callback); // 220 + } // 221 +}; // 222 + // 223 +// Returns the Mongo Collection object; may yield. // 224 +MongoConnection.prototype._getCollection = function (collectionName) { // 225 + var self = this; // 226 + // 227 + var future = new Future; // 228 + self._withDb(function (db) { // 229 + db.collection(collectionName, future.resolver()); // 230 + }); // 231 + return future.wait(); // 232 +}; // 233 + // 234 +MongoConnection.prototype._createCappedCollection = function (collectionName, // 235 + byteSize) { // 236 + var self = this; // 237 + var future = new Future(); // 238 + self._withDb(function (db) { // 239 + db.createCollection(collectionName, {capped: true, size: byteSize}, // 240 + future.resolver()); // 241 + }); // 242 + future.wait(); // 243 +}; // 244 + // 245 +// This should be called synchronously with a write, to create a // 246 +// transaction on the current write fence, if any. After we can read // 247 +// the write, and after observers have been notified (or at least, // 248 +// after the observer notifiers have added themselves to the write // 249 +// fence), you should call 'committed()' on the object returned. // 250 +MongoConnection.prototype._maybeBeginWrite = function () { // 251 + var self = this; // 252 + var fence = DDPServer._CurrentWriteFence.get(); // 253 + if (fence) // 254 + return fence.beginWrite(); // 255 + else // 256 + return {committed: function () {}}; // 257 +}; // 258 + // 259 +// Internal interface: adds a callback which is called when the Mongo primary // 260 +// changes. Returns a stop handle. // 261 +MongoConnection.prototype._onFailover = function (callback) { // 262 + return this._onFailoverHook.register(callback); // 263 +}; // 264 + // 265 + // 266 +//////////// Public API ////////// // 267 + // 268 +// The write methods block until the database has confirmed the write (it may // 269 +// not be replicated or stable on disk, but one server has confirmed it) if no // 270 +// callback is provided. If a callback is provided, then they call the callback // 271 +// when the write is confirmed. They return nothing on success, and raise an // 272 +// exception on failure. // 273 +// // 274 +// After making a write (with insert, update, remove), observers are // 275 +// notified asynchronously. If you want to receive a callback once all // 276 +// of the observer notifications have landed for your write, do the // 277 +// writes inside a write fence (set DDPServer._CurrentWriteFence to a new // 278 +// _WriteFence, and then set a callback on the write fence.) // 279 +// // 280 +// Since our execution environment is single-threaded, this is // 281 +// well-defined -- a write "has been made" if it's returned, and an // 282 +// observer "has been notified" if its callback has returned. // 283 + // 284 +var writeCallback = function (write, refresh, callback) { // 285 + return function (err, result) { // 286 + if (! err) { // 287 + // XXX We don't have to run this on error, right? // 288 + refresh(); // 289 + } // 290 + write.committed(); // 291 + if (callback) // 292 + callback(err, result); // 293 + else if (err) // 294 + throw err; // 295 + }; // 296 +}; // 297 + // 298 +var bindEnvironmentForWrite = function (callback) { // 299 + return Meteor.bindEnvironment(callback, "Mongo write"); // 300 +}; // 301 + // 302 +MongoConnection.prototype._insert = function (collection_name, document, // 303 + callback) { // 304 + var self = this; // 305 + // 306 + var sendError = function (e) { // 307 + if (callback) // 308 + return callback(e); // 309 + throw e; // 310 + }; // 311 + // 312 + if (collection_name === "___meteor_failure_test_collection") { // 313 + var e = new Error("Failure test"); // 314 + e.expected = true; // 315 + sendError(e); // 316 + return; // 317 + } // 318 + // 319 + if (!(LocalCollection._isPlainObject(document) && // 320 + !EJSON._isCustomType(document))) { // 321 + sendError(new Error( // 322 + "Only documents (plain objects) may be inserted into MongoDB")); // 323 + return; // 324 + } // 325 + // 326 + var write = self._maybeBeginWrite(); // 327 + var refresh = function () { // 328 + Meteor.refresh({collection: collection_name, id: document._id }); // 329 + }; // 330 + callback = bindEnvironmentForWrite(writeCallback(write, refresh, callback)); // 331 + try { // 332 + var collection = self._getCollection(collection_name); // 333 + collection.insert(replaceTypes(document, replaceMeteorAtomWithMongo), // 334 + {safe: true}, callback); // 335 + } catch (e) { // 336 + write.committed(); // 337 + throw e; // 338 + } // 339 +}; // 340 + // 341 +// Cause queries that may be affected by the selector to poll in this write // 342 +// fence. // 343 +MongoConnection.prototype._refresh = function (collectionName, selector) { // 344 + var self = this; // 345 + var refreshKey = {collection: collectionName}; // 346 + // If we know which documents we're removing, don't poll queries that are // 347 + // specific to other documents. (Note that multiple notifications here should // 348 + // not cause multiple polls, since all our listener is doing is enqueueing a // 349 + // poll.) // 350 + var specificIds = LocalCollection._idsMatchedBySelector(selector); // 351 + if (specificIds) { // 352 + _.each(specificIds, function (id) { // 353 + Meteor.refresh(_.extend({id: id}, refreshKey)); // 354 + }); // 355 + } else { // 356 + Meteor.refresh(refreshKey); // 357 + } // 358 +}; // 359 + // 360 +MongoConnection.prototype._remove = function (collection_name, selector, // 361 + callback) { // 362 + var self = this; // 363 + // 364 + if (collection_name === "___meteor_failure_test_collection") { // 365 + var e = new Error("Failure test"); // 366 + e.expected = true; // 367 + if (callback) // 368 + return callback(e); // 369 + else // 370 + throw e; // 371 + } // 372 + // 373 + var write = self._maybeBeginWrite(); // 374 + var refresh = function () { // 375 + self._refresh(collection_name, selector); // 376 + }; // 377 + callback = bindEnvironmentForWrite(writeCallback(write, refresh, callback)); // 378 + // 379 + try { // 380 + var collection = self._getCollection(collection_name); // 381 + collection.remove(replaceTypes(selector, replaceMeteorAtomWithMongo), // 382 + {safe: true}, callback); // 383 + } catch (e) { // 384 + write.committed(); // 385 + throw e; // 386 + } // 387 +}; // 388 + // 389 +MongoConnection.prototype._dropCollection = function (collectionName, cb) { // 390 + var self = this; // 391 + // 392 + var write = self._maybeBeginWrite(); // 393 + var refresh = function () { // 394 + Meteor.refresh({collection: collectionName, id: null, // 395 + dropCollection: true}); // 396 + }; // 397 + cb = bindEnvironmentForWrite(writeCallback(write, refresh, cb)); // 398 + // 399 + try { // 400 + var collection = self._getCollection(collectionName); // 401 + collection.drop(cb); // 402 + } catch (e) { // 403 + write.committed(); // 404 + throw e; // 405 + } // 406 +}; // 407 + // 408 +MongoConnection.prototype._update = function (collection_name, selector, mod, // 409 + options, callback) { // 410 + var self = this; // 411 + // 412 + if (! callback && options instanceof Function) { // 413 + callback = options; // 414 + options = null; // 415 + } // 416 + // 417 + if (collection_name === "___meteor_failure_test_collection") { // 418 + var e = new Error("Failure test"); // 419 + e.expected = true; // 420 + if (callback) // 421 + return callback(e); // 422 + else // 423 + throw e; // 424 + } // 425 + // 426 + // explicit safety check. null and undefined can crash the mongo // 427 + // driver. Although the node driver and minimongo do 'support' // 428 + // non-object modifier in that they don't crash, they are not // 429 + // meaningful operations and do not do anything. Defensively throw an // 430 + // error here. // 431 + if (!mod || typeof mod !== 'object') // 432 + throw new Error("Invalid modifier. Modifier must be an object."); // 433 + // 434 + if (!options) options = {}; // 435 + // 436 + var write = self._maybeBeginWrite(); // 437 + var refresh = function () { // 438 + self._refresh(collection_name, selector); // 439 + }; // 440 + callback = writeCallback(write, refresh, callback); // 441 + try { // 442 + var collection = self._getCollection(collection_name); // 443 + var mongoOpts = {safe: true}; // 444 + // explictly enumerate options that minimongo supports // 445 + if (options.upsert) mongoOpts.upsert = true; // 446 + if (options.multi) mongoOpts.multi = true; // 447 + // 448 + var mongoSelector = replaceTypes(selector, replaceMeteorAtomWithMongo); // 449 + var mongoMod = replaceTypes(mod, replaceMeteorAtomWithMongo); // 450 + // 451 + var isModify = isModificationMod(mongoMod); // 452 + var knownId = (isModify ? selector._id : mod._id); // 453 + // 454 + if (options.upsert && (! knownId) && options.insertedId) { // 455 + // XXX In future we could do a real upsert for the mongo id generation // 456 + // case, if the the node mongo driver gives us back the id of the upserted // 457 + // doc (which our current version does not). // 458 + simulateUpsertWithInsertedId( // 459 + collection, mongoSelector, mongoMod, // 460 + isModify, options, // 461 + // This callback does not need to be bindEnvironment'ed because // 462 + // simulateUpsertWithInsertedId() wraps it and then passes it through // 463 + // bindEnvironmentForWrite. // 464 + function (err, result) { // 465 + // If we got here via a upsert() call, then options._returnObject will // 466 + // be set and we should return the whole object. Otherwise, we should // 467 + // just return the number of affected docs to match the mongo API. // 468 + if (result && ! options._returnObject) // 469 + callback(err, result.numberAffected); // 470 + else // 471 + callback(err, result); // 472 + } // 473 + ); // 474 + } else { // 475 + collection.update( // 476 + mongoSelector, mongoMod, mongoOpts, // 477 + bindEnvironmentForWrite(function (err, result, extra) { // 478 + if (! err) { // 479 + if (result && options._returnObject) { // 480 + result = { numberAffected: result }; // 481 + // If this was an upsert() call, and we ended up // 482 + // inserting a new doc and we know its id, then // 483 + // return that id as well. // 484 + if (options.upsert && knownId && // 485 + ! extra.updatedExisting) // 486 + result.insertedId = knownId; // 487 + } // 488 + } // 489 + callback(err, result); // 490 + })); // 491 + } // 492 + } catch (e) { // 493 + write.committed(); // 494 + throw e; // 495 + } // 496 +}; // 497 + // 498 +var isModificationMod = function (mod) { // 499 + for (var k in mod) // 500 + if (k.substr(0, 1) === '$') // 501 + return true; // 502 + return false; // 503 +}; // 504 + // 505 +var NUM_OPTIMISTIC_TRIES = 3; // 506 + // 507 +// exposed for testing // 508 +MongoConnection._isCannotChangeIdError = function (err) { // 509 + // either of these checks should work, but just to be safe... // 510 + return (err.code === 13596 || // 511 + err.err.indexOf("cannot change _id of a document") === 0); // 512 +}; // 513 + // 514 +var simulateUpsertWithInsertedId = function (collection, selector, mod, // 515 + isModify, options, callback) { // 516 + // STRATEGY: First try doing a plain update. If it affected 0 documents, // 517 + // then without affecting the database, we know we should probably do an // 518 + // insert. We then do a *conditional* insert that will fail in the case // 519 + // of a race condition. This conditional insert is actually an // 520 + // upsert-replace with an _id, which will never successfully update an // 521 + // existing document. If this upsert fails with an error saying it // 522 + // couldn't change an existing _id, then we know an intervening write has // 523 + // caused the query to match something. We go back to step one and repeat. // 524 + // Like all "optimistic write" schemes, we rely on the fact that it's // 525 + // unlikely our writes will continue to be interfered with under normal // 526 + // circumstances (though sufficiently heavy contention with writers // 527 + // disagreeing on the existence of an object will cause writes to fail // 528 + // in theory). // 529 + // 530 + var newDoc; // 531 + // Run this code up front so that it fails fast if someone uses // 532 + // a Mongo update operator we don't support. // 533 + if (isModify) { // 534 + // We've already run replaceTypes/replaceMeteorAtomWithMongo on // 535 + // selector and mod. We assume it doesn't matter, as far as // 536 + // the behavior of modifiers is concerned, whether `_modify` // 537 + // is run on EJSON or on mongo-converted EJSON. // 538 + var selectorDoc = LocalCollection._removeDollarOperators(selector); // 539 + LocalCollection._modify(selectorDoc, mod, {isInsert: true}); // 540 + newDoc = selectorDoc; // 541 + } else { // 542 + newDoc = mod; // 543 + } // 544 + // 545 + var insertedId = options.insertedId; // must exist // 546 + var mongoOptsForUpdate = { // 547 + safe: true, // 548 + multi: options.multi // 549 + }; // 550 + var mongoOptsForInsert = { // 551 + safe: true, // 552 + upsert: true // 553 + }; // 554 + // 555 + var tries = NUM_OPTIMISTIC_TRIES; // 556 + // 557 + var doUpdate = function () { // 558 + tries--; // 559 + if (! tries) { // 560 + callback(new Error("Upsert failed after " + NUM_OPTIMISTIC_TRIES + " tries.")); // 561 + } else { // 562 + collection.update(selector, mod, mongoOptsForUpdate, // 563 + bindEnvironmentForWrite(function (err, result) { // 564 + if (err) // 565 + callback(err); // 566 + else if (result) // 567 + callback(null, { // 568 + numberAffected: result // 569 + }); // 570 + else // 571 + doConditionalInsert(); // 572 + })); // 573 + } // 574 + }; // 575 + // 576 + var doConditionalInsert = function () { // 577 + var replacementWithId = _.extend( // 578 + replaceTypes({_id: insertedId}, replaceMeteorAtomWithMongo), // 579 + newDoc); // 580 + collection.update(selector, replacementWithId, mongoOptsForInsert, // 581 + bindEnvironmentForWrite(function (err, result) { // 582 + if (err) { // 583 + // figure out if this is a // 584 + // "cannot change _id of document" error, and // 585 + // if so, try doUpdate() again, up to 3 times. // 586 + if (MongoConnection._isCannotChangeIdError(err)) { // 587 + doUpdate(); // 588 + } else { // 589 + callback(err); // 590 + } // 591 + } else { // 592 + callback(null, { // 593 + numberAffected: result, // 594 + insertedId: insertedId // 595 + }); // 596 + } // 597 + })); // 598 + }; // 599 + // 600 + doUpdate(); // 601 +}; // 602 + // 603 +_.each(["insert", "update", "remove", "dropCollection"], function (method) { // 604 + MongoConnection.prototype[method] = function (/* arguments */) { // 605 + var self = this; // 606 + return Meteor._wrapAsync(self["_" + method]).apply(self, arguments); // 607 + }; // 608 +}); // 609 + // 610 +// XXX MongoConnection.upsert() does not return the id of the inserted document // 611 +// unless you set it explicitly in the selector or modifier (as a replacement // 612 +// doc). // 613 +MongoConnection.prototype.upsert = function (collectionName, selector, mod, // 614 + options, callback) { // 615 + var self = this; // 616 + if (typeof options === "function" && ! callback) { // 617 + callback = options; // 618 + options = {}; // 619 + } // 620 + // 621 + return self.update(collectionName, selector, mod, // 622 + _.extend({}, options, { // 623 + upsert: true, // 624 + _returnObject: true // 625 + }), callback); // 626 +}; // 627 + // 628 +MongoConnection.prototype.find = function (collectionName, selector, options) { // 629 + var self = this; // 630 + // 631 + if (arguments.length === 1) // 632 + selector = {}; // 633 + // 634 + return new Cursor( // 635 + self, new CursorDescription(collectionName, selector, options)); // 636 +}; // 637 + // 638 +MongoConnection.prototype.findOne = function (collection_name, selector, // 639 + options) { // 640 + var self = this; // 641 + if (arguments.length === 1) // 642 + selector = {}; // 643 + // 644 + options = options || {}; // 645 + options.limit = 1; // 646 + return self.find(collection_name, selector, options).fetch()[0]; // 647 +}; // 648 + // 649 +// We'll actually design an index API later. For now, we just pass through to // 650 +// Mongo's, but make it synchronous. // 651 +MongoConnection.prototype._ensureIndex = function (collectionName, index, // 652 + options) { // 653 + var self = this; // 654 + options = _.extend({safe: true}, options); // 655 + // 656 + // We expect this function to be called at startup, not from within a method, // 657 + // so we don't interact with the write fence. // 658 + var collection = self._getCollection(collectionName); // 659 + var future = new Future; // 660 + var indexName = collection.ensureIndex(index, options, future.resolver()); // 661 + future.wait(); // 662 +}; // 663 +MongoConnection.prototype._dropIndex = function (collectionName, index) { // 664 + var self = this; // 665 + // 666 + // This function is only used by test code, not within a method, so we don't // 667 + // interact with the write fence. // 668 + var collection = self._getCollection(collectionName); // 669 + var future = new Future; // 670 + var indexName = collection.dropIndex(index, future.resolver()); // 671 + future.wait(); // 672 +}; // 673 + // 674 +// CURSORS // 675 + // 676 +// There are several classes which relate to cursors: // 677 +// // 678 +// CursorDescription represents the arguments used to construct a cursor: // 679 +// collectionName, selector, and (find) options. Because it is used as a key // 680 +// for cursor de-dup, everything in it should either be JSON-stringifiable or // 681 +// not affect observeChanges output (eg, options.transform functions are not // 682 +// stringifiable but do not affect observeChanges). // 683 +// // 684 +// SynchronousCursor is a wrapper around a MongoDB cursor // 685 +// which includes fully-synchronous versions of forEach, etc. // 686 +// // 687 +// Cursor is the cursor object returned from find(), which implements the // 688 +// documented Meteor.Collection cursor API. It wraps a CursorDescription and a // 689 +// SynchronousCursor (lazily: it doesn't contact Mongo until you call a method // 690 +// like fetch or forEach on it). // 691 +// // 692 +// ObserveHandle is the "observe handle" returned from observeChanges. It has a // 693 +// reference to an ObserveMultiplexer. // 694 +// // 695 +// ObserveMultiplexer allows multiple identical ObserveHandles to be driven by a // 696 +// single observe driver. // 697 +// // 698 +// There are two "observe drivers" which drive ObserveMultiplexers: // 699 +// - PollingObserveDriver caches the results of a query and reruns it when // 700 +// necessary. // 701 +// - OplogObserveDriver follows the Mongo operation log to directly observe // 702 +// database changes. // 703 +// Both implementations follow the same simple interface: when you create them, // 704 +// they start sending observeChanges callbacks (and a ready() invocation) to // 705 +// their ObserveMultiplexer, and you stop them by calling their stop() method. // 706 + // 707 +CursorDescription = function (collectionName, selector, options) { // 708 + var self = this; // 709 + self.collectionName = collectionName; // 710 + self.selector = Meteor.Collection._rewriteSelector(selector); // 711 + self.options = options || {}; // 712 +}; // 713 + // 714 +Cursor = function (mongo, cursorDescription) { // 715 + var self = this; // 716 + // 717 + self._mongo = mongo; // 718 + self._cursorDescription = cursorDescription; // 719 + self._synchronousCursor = null; // 720 +}; // 721 + // 722 +_.each(['forEach', 'map', 'fetch', 'count'], function (method) { // 723 + Cursor.prototype[method] = function () { // 724 + var self = this; // 725 + // 726 + // You can only observe a tailable cursor. // 727 + if (self._cursorDescription.options.tailable) // 728 + throw new Error("Cannot call " + method + " on a tailable cursor"); // 729 + // 730 + if (!self._synchronousCursor) { // 731 + self._synchronousCursor = self._mongo._createSynchronousCursor( // 732 + self._cursorDescription, { // 733 + // Make sure that the "self" argument to forEach/map callbacks is the // 734 + // Cursor, not the SynchronousCursor. // 735 + selfForIteration: self, // 736 + useTransform: true // 737 + }); // 738 + } // 739 + // 740 + return self._synchronousCursor[method].apply( // 741 + self._synchronousCursor, arguments); // 742 + }; // 743 +}); // 744 + // 745 +// Since we don't actually have a "nextObject" interface, there's really no // 746 +// reason to have a "rewind" interface. All it did was make multiple calls // 747 +// to fetch/map/forEach return nothing the second time. // 748 +// XXX COMPAT WITH 0.8.1 // 749 +Cursor.prototype.rewind = function () { // 750 +}; // 751 + // 752 +Cursor.prototype.getTransform = function () { // 753 + return this._cursorDescription.options.transform; // 754 +}; // 755 + // 756 +// When you call Meteor.publish() with a function that returns a Cursor, we need // 757 +// to transmute it into the equivalent subscription. This is the function that // 758 +// does that. // 759 + // 760 +Cursor.prototype._publishCursor = function (sub) { // 761 + var self = this; // 762 + var collection = self._cursorDescription.collectionName; // 763 + return Meteor.Collection._publishCursor(self, sub, collection); // 764 +}; // 765 + // 766 +// Used to guarantee that publish functions return at most one cursor per // 767 +// collection. Private, because we might later have cursors that include // 768 +// documents from multiple collections somehow. // 769 +Cursor.prototype._getCollectionName = function () { // 770 + var self = this; // 771 + return self._cursorDescription.collectionName; // 772 +} // 773 + // 774 +Cursor.prototype.observe = function (callbacks) { // 775 + var self = this; // 776 + return LocalCollection._observeFromObserveChanges(self, callbacks); // 777 +}; // 778 + // 779 +Cursor.prototype.observeChanges = function (callbacks) { // 780 + var self = this; // 781 + var ordered = LocalCollection._observeChangesCallbacksAreOrdered(callbacks); // 782 + return self._mongo._observeChanges( // 783 + self._cursorDescription, ordered, callbacks); // 784 +}; // 785 + // 786 +MongoConnection.prototype._createSynchronousCursor = function( // 787 + cursorDescription, options) { // 788 + var self = this; // 789 + options = _.pick(options || {}, 'selfForIteration', 'useTransform'); // 790 + // 791 + var collection = self._getCollection(cursorDescription.collectionName); // 792 + var cursorOptions = cursorDescription.options; // 793 + var mongoOptions = { // 794 + sort: cursorOptions.sort, // 795 + limit: cursorOptions.limit, // 796 + skip: cursorOptions.skip // 797 + }; // 798 + // 799 + // Do we want a tailable cursor (which only works on capped collections)? // 800 + if (cursorOptions.tailable) { // 801 + // We want a tailable cursor... // 802 + mongoOptions.tailable = true; // 803 + // ... and for the server to wait a bit if any getMore has no data (rather // 804 + // than making us put the relevant sleeps in the client)... // 805 + mongoOptions.awaitdata = true; // 806 + // ... and to keep querying the server indefinitely rather than just 5 times // 807 + // if there's no more data. // 808 + mongoOptions.numberOfRetries = -1; // 809 + // And if this is on the oplog collection and the cursor specifies a 'ts', // 810 + // then set the undocumented oplog replay flag, which does a special scan to // 811 + // find the first document (instead of creating an index on ts). This is a // 812 + // very hard-coded Mongo flag which only works on the oplog collection and // 813 + // only works with the ts field. // 814 + if (cursorDescription.collectionName === OPLOG_COLLECTION && // 815 + cursorDescription.selector.ts) { // 816 + mongoOptions.oplogReplay = true; // 817 + } // 818 + } // 819 + // 820 + var dbCursor = collection.find( // 821 + replaceTypes(cursorDescription.selector, replaceMeteorAtomWithMongo), // 822 + cursorOptions.fields, mongoOptions); // 823 + // 824 + return new SynchronousCursor(dbCursor, cursorDescription, options); // 825 +}; // 826 + // 827 +var SynchronousCursor = function (dbCursor, cursorDescription, options) { // 828 + var self = this; // 829 + options = _.pick(options || {}, 'selfForIteration', 'useTransform'); // 830 + // 831 + self._dbCursor = dbCursor; // 832 + self._cursorDescription = cursorDescription; // 833 + // The "self" argument passed to forEach/map callbacks. If we're wrapped // 834 + // inside a user-visible Cursor, we want to provide the outer cursor! // 835 + self._selfForIteration = options.selfForIteration || self; // 836 + if (options.useTransform && cursorDescription.options.transform) { // 837 + self._transform = LocalCollection.wrapTransform( // 838 + cursorDescription.options.transform); // 839 + } else { // 840 + self._transform = null; // 841 + } // 842 + // 843 + // Need to specify that the callback is the first argument to nextObject, // 844 + // since otherwise when we try to call it with no args the driver will // 845 + // interpret "undefined" first arg as an options hash and crash. // 846 + self._synchronousNextObject = Future.wrap( // 847 + dbCursor.nextObject.bind(dbCursor), 0); // 848 + self._synchronousCount = Future.wrap(dbCursor.count.bind(dbCursor)); // 849 + self._visitedIds = new LocalCollection._IdMap; // 850 +}; // 851 + // 852 +_.extend(SynchronousCursor.prototype, { // 853 + _nextObject: function () { // 854 + var self = this; // 855 + // 856 + while (true) { // 857 + var doc = self._synchronousNextObject().wait(); // 858 + // 859 + if (!doc) return null; // 860 + doc = replaceTypes(doc, replaceMongoAtomWithMeteor); // 861 + // 862 + if (!self._cursorDescription.options.tailable && _.has(doc, '_id')) { // 863 + // Did Mongo give us duplicate documents in the same cursor? If so, // 864 + // ignore this one. (Do this before the transform, since transform might // 865 + // return some unrelated value.) We don't do this for tailable cursors, // 866 + // because we want to maintain O(1) memory usage. And if there isn't _id // 867 + // for some reason (maybe it's the oplog), then we don't do this either. // 868 + // (Be careful to do this for falsey but existing _id, though.) // 869 + if (self._visitedIds.has(doc._id)) continue; // 870 + self._visitedIds.set(doc._id, true); // 871 + } // 872 + // 873 + if (self._transform) // 874 + doc = self._transform(doc); // 875 + // 876 + return doc; // 877 + } // 878 + }, // 879 + // 880 + forEach: function (callback, thisArg) { // 881 + var self = this; // 882 + // 883 + // Get back to the beginning. // 884 + self._rewind(); // 885 + // 886 + // We implement the loop ourself instead of using self._dbCursor.each, // 887 + // because "each" will call its callback outside of a fiber which makes it // 888 + // much more complex to make this function synchronous. // 889 + var index = 0; // 890 + while (true) { // 891 + var doc = self._nextObject(); // 892 + if (!doc) return; // 893 + callback.call(thisArg, doc, index++, self._selfForIteration); // 894 + } // 895 + }, // 896 + // 897 + // XXX Allow overlapping callback executions if callback yields. // 898 + map: function (callback, thisArg) { // 899 + var self = this; // 900 + var res = []; // 901 + self.forEach(function (doc, index) { // 902 + res.push(callback.call(thisArg, doc, index, self._selfForIteration)); // 903 + }); // 904 + return res; // 905 + }, // 906 + // 907 + _rewind: function () { // 908 + var self = this; // 909 + // 910 + // known to be synchronous // 911 + self._dbCursor.rewind(); // 912 + // 913 + self._visitedIds = new LocalCollection._IdMap; // 914 + }, // 915 + // 916 + // Mostly usable for tailable cursors. // 917 + close: function () { // 918 + var self = this; // 919 + // 920 + self._dbCursor.close(); // 921 + }, // 922 + // 923 + fetch: function () { // 924 + var self = this; // 925 + return self.map(_.identity); // 926 + }, // 927 + // 928 + count: function () { // 929 + var self = this; // 930 + return self._synchronousCount().wait(); // 931 + }, // 932 + // 933 + // This method is NOT wrapped in Cursor. // 934 + getRawObjects: function (ordered) { // 935 + var self = this; // 936 + if (ordered) { // 937 + return self.fetch(); // 938 + } else { // 939 + var results = new LocalCollection._IdMap; // 940 + self.forEach(function (doc) { // 941 + results.set(doc._id, doc); // 942 + }); // 943 + return results; // 944 + } // 945 + } // 946 +}); // 947 + // 948 +MongoConnection.prototype.tail = function (cursorDescription, docCallback) { // 949 + var self = this; // 950 + if (!cursorDescription.options.tailable) // 951 + throw new Error("Can only tail a tailable cursor"); // 952 + // 953 + var cursor = self._createSynchronousCursor(cursorDescription); // 954 + // 955 + var stopped = false; // 956 + var lastTS = undefined; // 957 + var loop = function () { // 958 + while (true) { // 959 + if (stopped) // 960 + return; // 961 + try { // 962 + var doc = cursor._nextObject(); // 963 + } catch (err) { // 964 + // There's no good way to figure out if this was actually an error // 965 + // from Mongo. Ah well. But either way, we need to retry the cursor // 966 + // (unless the failure was because the observe got stopped). // 967 + doc = null; // 968 + } // 969 + // Since cursor._nextObject can yield, we need to check again to see if // 970 + // we've been stopped before calling the callback. // 971 + if (stopped) // 972 + return; // 973 + if (doc) { // 974 + // If a tailable cursor contains a "ts" field, use it to recreate the // 975 + // cursor on error. ("ts" is a standard that Mongo uses internally for // 976 + // the oplog, and there's a special flag that lets you do binary search // 977 + // on it instead of needing to use an index.) // 978 + lastTS = doc.ts; // 979 + docCallback(doc); // 980 + } else { // 981 + var newSelector = _.clone(cursorDescription.selector); // 982 + if (lastTS) { // 983 + newSelector.ts = {$gt: lastTS}; // 984 + } // 985 + cursor = self._createSynchronousCursor(new CursorDescription( // 986 + cursorDescription.collectionName, // 987 + newSelector, // 988 + cursorDescription.options)); // 989 + // Mongo failover takes many seconds. Retry in a bit. (Without this // 990 + // setTimeout, we peg the CPU at 100% and never notice the actual // 991 + // failover. // 992 + Meteor.setTimeout(loop, 100); // 993 + break; // 994 + } // 995 + } // 996 + }; // 997 + // 998 + Meteor.defer(loop); // 999 + // 1000 + return { // 1001 + stop: function () { // 1002 + stopped = true; // 1003 + cursor.close(); // 1004 + } // 1005 + }; // 1006 +}; // 1007 + // 1008 +MongoConnection.prototype._observeChanges = function ( // 1009 + cursorDescription, ordered, callbacks) { // 1010 + var self = this; // 1011 + // 1012 + if (cursorDescription.options.tailable) { // 1013 + return self._observeChangesTailable(cursorDescription, ordered, callbacks); // 1014 + } // 1015 + // 1016 + // You may not filter out _id when observing changes, because the id is a core // 1017 + // part of the observeChanges API. // 1018 + if (cursorDescription.options.fields && // 1019 + (cursorDescription.options.fields._id === 0 || // 1020 + cursorDescription.options.fields._id === false)) { // 1021 + throw Error("You may not observe a cursor with {fields: {_id: 0}}"); // 1022 + } // 1023 + // 1024 + var observeKey = JSON.stringify( // 1025 + _.extend({ordered: ordered}, cursorDescription)); // 1026 + // 1027 + var multiplexer, observeDriver; // 1028 + var firstHandle = false; // 1029 + // 1030 + // Find a matching ObserveMultiplexer, or create a new one. This next block is // 1031 + // guaranteed to not yield (and it doesn't call anything that can observe a // 1032 + // new query), so no other calls to this function can interleave with it. // 1033 + Meteor._noYieldsAllowed(function () { // 1034 + if (_.has(self._observeMultiplexers, observeKey)) { // 1035 + multiplexer = self._observeMultiplexers[observeKey]; // 1036 + } else { // 1037 + firstHandle = true; // 1038 + // Create a new ObserveMultiplexer. // 1039 + multiplexer = new ObserveMultiplexer({ // 1040 + ordered: ordered, // 1041 + onStop: function () { // 1042 + observeDriver.stop(); // 1043 + delete self._observeMultiplexers[observeKey]; // 1044 + } // 1045 + }); // 1046 + self._observeMultiplexers[observeKey] = multiplexer; // 1047 + } // 1048 + }); // 1049 + // 1050 + var observeHandle = new ObserveHandle(multiplexer, callbacks); // 1051 + // 1052 + if (firstHandle) { // 1053 + var matcher, sorter; // 1054 + var canUseOplog = _.all([ // 1055 + function () { // 1056 + // At a bare minimum, using the oplog requires us to have an oplog, to // 1057 + // want unordered callbacks, and to not want a callback on the polls // 1058 + // that won't happen. // 1059 + return self._oplogHandle && !ordered && // 1060 + !callbacks._testOnlyPollCallback; // 1061 + }, function () { // 1062 + // We need to be able to compile the selector. Fall back to polling for // 1063 + // some newfangled $selector that minimongo doesn't support yet. // 1064 + try { // 1065 + matcher = new Minimongo.Matcher(cursorDescription.selector); // 1066 + return true; // 1067 + } catch (e) { // 1068 + // XXX make all compilation errors MinimongoError or something // 1069 + // so that this doesn't ignore unrelated exceptions // 1070 + return false; // 1071 + } // 1072 + }, function () { // 1073 + // ... and the selector itself needs to support oplog. // 1074 + return OplogObserveDriver.cursorSupported(cursorDescription, matcher); // 1075 + }, function () { // 1076 + // And we need to be able to compile the sort, if any. eg, can't be // 1077 + // {$natural: 1}. // 1078 + if (!cursorDescription.options.sort) // 1079 + return true; // 1080 + try { // 1081 + sorter = new Minimongo.Sorter(cursorDescription.options.sort, // 1082 + { matcher: matcher }); // 1083 + return true; // 1084 + } catch (e) { // 1085 + // XXX make all compilation errors MinimongoError or something // 1086 + // so that this doesn't ignore unrelated exceptions // 1087 + return false; // 1088 + } // 1089 + }], function (f) { return f(); }); // invoke each function // 1090 + // 1091 + var driverClass = canUseOplog ? OplogObserveDriver : PollingObserveDriver; // 1092 + observeDriver = new driverClass({ // 1093 + cursorDescription: cursorDescription, // 1094 + mongoHandle: self, // 1095 + multiplexer: multiplexer, // 1096 + ordered: ordered, // 1097 + matcher: matcher, // ignored by polling // 1098 + sorter: sorter, // ignored by polling // 1099 + _testOnlyPollCallback: callbacks._testOnlyPollCallback // 1100 + }); // 1101 + // 1102 + // This field is only set for use in tests. // 1103 + multiplexer._observeDriver = observeDriver; // 1104 + } // 1105 + // 1106 + // Blocks until the initial adds have been sent. // 1107 + multiplexer.addHandleAndSendInitialAdds(observeHandle); // 1108 + // 1109 + return observeHandle; // 1110 +}; // 1111 + // 1112 +// Listen for the invalidation messages that will trigger us to poll the // 1113 +// database for changes. If this selector specifies specific IDs, specify them // 1114 +// here, so that updates to different specific IDs don't cause us to poll. // 1115 +// listenCallback is the same kind of (notification, complete) callback passed // 1116 +// to InvalidationCrossbar.listen. // 1117 + // 1118 +listenAll = function (cursorDescription, listenCallback) { // 1119 + var listeners = []; // 1120 + forEachTrigger(cursorDescription, function (trigger) { // 1121 + listeners.push(DDPServer._InvalidationCrossbar.listen( // 1122 + trigger, listenCallback)); // 1123 + }); // 1124 + // 1125 + return { // 1126 + stop: function () { // 1127 + _.each(listeners, function (listener) { // 1128 + listener.stop(); // 1129 + }); // 1130 + } // 1131 + }; // 1132 +}; // 1133 + // 1134 +forEachTrigger = function (cursorDescription, triggerCallback) { // 1135 + var key = {collection: cursorDescription.collectionName}; // 1136 + var specificIds = LocalCollection._idsMatchedBySelector( // 1137 + cursorDescription.selector); // 1138 + if (specificIds) { // 1139 + _.each(specificIds, function (id) { // 1140 + triggerCallback(_.extend({id: id}, key)); // 1141 + }); // 1142 + triggerCallback(_.extend({dropCollection: true, id: null}, key)); // 1143 + } else { // 1144 + triggerCallback(key); // 1145 + } // 1146 +}; // 1147 + // 1148 +// observeChanges for tailable cursors on capped collections. // 1149 +// // 1150 +// Some differences from normal cursors: // 1151 +// - Will never produce anything other than 'added' or 'addedBefore'. If you // 1152 +// do update a document that has already been produced, this will not notice // 1153 +// it. // 1154 +// - If you disconnect and reconnect from Mongo, it will essentially restart // 1155 +// the query, which will lead to duplicate results. This is pretty bad, // 1156 +// but if you include a field called 'ts' which is inserted as // 1157 +// new MongoInternals.MongoTimestamp(0, 0) (which is initialized to the // 1158 +// current Mongo-style timestamp), we'll be able to find the place to // 1159 +// restart properly. (This field is specifically understood by Mongo with an // 1160 +// optimization which allows it to find the right place to start without // 1161 +// an index on ts. It's how the oplog works.) // 1162 +// - No callbacks are triggered synchronously with the call (there's no // 1163 +// differentiation between "initial data" and "later changes"; everything // 1164 +// that matches the query gets sent asynchronously). // 1165 +// - De-duplication is not implemented. // 1166 +// - Does not yet interact with the write fence. Probably, this should work by // 1167 +// ignoring removes (which don't work on capped collections) and updates // 1168 +// (which don't affect tailable cursors), and just keeping track of the ID // 1169 +// of the inserted object, and closing the write fence once you get to that // 1170 +// ID (or timestamp?). This doesn't work well if the document doesn't match // 1171 +// the query, though. On the other hand, the write fence can close // 1172 +// immediately if it does not match the query. So if we trust minimongo // 1173 +// enough to accurately evaluate the query against the write fence, we // 1174 +// should be able to do this... Of course, minimongo doesn't even support // 1175 +// Mongo Timestamps yet. // 1176 +MongoConnection.prototype._observeChangesTailable = function ( // 1177 + cursorDescription, ordered, callbacks) { // 1178 + var self = this; // 1179 + // 1180 + // Tailable cursors only ever call added/addedBefore callbacks, so it's an // 1181 + // error if you didn't provide them. // 1182 + if ((ordered && !callbacks.addedBefore) || // 1183 + (!ordered && !callbacks.added)) { // 1184 + throw new Error("Can't observe an " + (ordered ? "ordered" : "unordered") // 1185 + + " tailable cursor without a " // 1186 + + (ordered ? "addedBefore" : "added") + " callback"); // 1187 + } // 1188 + // 1189 + return self.tail(cursorDescription, function (doc) { // 1190 + var id = doc._id; // 1191 + delete doc._id; // 1192 + // The ts is an implementation detail. Hide it. // 1193 + delete doc.ts; // 1194 + if (ordered) { // 1195 + callbacks.addedBefore(id, doc, null); // 1196 + } else { // 1197 + callbacks.added(id, doc); // 1198 + } // 1199 + }); // 1200 +}; // 1201 + // 1202 +// XXX We probably need to find a better way to expose this. Right now // 1203 +// it's only used by tests, but in fact you need it in normal // 1204 +// operation to interact with capped collections (eg, Galaxy uses it). // 1205 +MongoInternals.MongoTimestamp = MongoDB.Timestamp; // 1206 + // 1207 +MongoInternals.Connection = MongoConnection; // 1208 +MongoInternals.NpmModule = MongoDB; // 1209 + // 1210 +///////////////////////////////////////////////////////////////////////////////////////////////////////// + +}).call(this); + + + + + + +(function () { + +///////////////////////////////////////////////////////////////////////////////////////////////////////// +// // +// packages/mongo-livedata/oplog_tailing.js // +// // +///////////////////////////////////////////////////////////////////////////////////////////////////////// + // +var Future = Npm.require('fibers/future'); // 1 + // 2 +OPLOG_COLLECTION = 'oplog.rs'; // 3 +var REPLSET_COLLECTION = 'system.replset'; // 4 + // 5 +// Like Perl's quotemeta: quotes all regexp metacharacters. See // 6 +// https://github.com/substack/quotemeta/blob/master/index.js // 7 +// XXX this is duplicated with accounts_server.js // 8 +var quotemeta = function (str) { // 9 + return String(str).replace(/(\W)/g, '\\$1'); // 10 +}; // 11 + // 12 +var showTS = function (ts) { // 13 + return "Timestamp(" + ts.getHighBits() + ", " + ts.getLowBits() + ")"; // 14 +}; // 15 + // 16 +idForOp = function (op) { // 17 + if (op.op === 'd') // 18 + return op.o._id; // 19 + else if (op.op === 'i') // 20 + return op.o._id; // 21 + else if (op.op === 'u') // 22 + return op.o2._id; // 23 + else if (op.op === 'c') // 24 + throw Error("Operator 'c' doesn't supply an object with id: " + // 25 + EJSON.stringify(op)); // 26 + else // 27 + throw Error("Unknown op: " + EJSON.stringify(op)); // 28 +}; // 29 + // 30 +OplogHandle = function (oplogUrl, dbName) { // 31 + var self = this; // 32 + self._oplogUrl = oplogUrl; // 33 + self._dbName = dbName; // 34 + // 35 + self._oplogLastEntryConnection = null; // 36 + self._oplogTailConnection = null; // 37 + self._stopped = false; // 38 + self._tailHandle = null; // 39 + self._readyFuture = new Future(); // 40 + self._crossbar = new DDPServer._Crossbar({ // 41 + factPackage: "mongo-livedata", factName: "oplog-watchers" // 42 + }); // 43 + self._lastProcessedTS = null; // 44 + self._baseOplogSelector = { // 45 + ns: new RegExp('^' + quotemeta(self._dbName) + '\\.'), // 46 + $or: [ // 47 + { op: {$in: ['i', 'u', 'd']} }, // 48 + // If it is not db.collection.drop(), ignore it // 49 + { op: 'c', 'o.drop': { $exists: true } }] // 50 + }; // 51 + // XXX doc // 52 + self._catchingUpFutures = []; // 53 + // 54 + self._startTailing(); // 55 +}; // 56 + // 57 +_.extend(OplogHandle.prototype, { // 58 + stop: function () { // 59 + var self = this; // 60 + if (self._stopped) // 61 + return; // 62 + self._stopped = true; // 63 + if (self._tailHandle) // 64 + self._tailHandle.stop(); // 65 + // XXX should close connections too // 66 + }, // 67 + onOplogEntry: function (trigger, callback) { // 68 + var self = this; // 69 + if (self._stopped) // 70 + throw new Error("Called onOplogEntry on stopped handle!"); // 71 + // 72 + // Calling onOplogEntry requires us to wait for the tailing to be ready. // 73 + self._readyFuture.wait(); // 74 + // 75 + var originalCallback = callback; // 76 + callback = Meteor.bindEnvironment(function (notification) { // 77 + // XXX can we avoid this clone by making oplog.js careful? // 78 + originalCallback(EJSON.clone(notification)); // 79 + }, function (err) { // 80 + Meteor._debug("Error in oplog callback", err.stack); // 81 + }); // 82 + var listenHandle = self._crossbar.listen(trigger, callback); // 83 + return { // 84 + stop: function () { // 85 + listenHandle.stop(); // 86 + } // 87 + }; // 88 + }, // 89 + // Calls `callback` once the oplog has been processed up to a point that is // 90 + // roughly "now": specifically, once we've processed all ops that are // 91 + // currently visible. // 92 + // XXX become convinced that this is actually safe even if oplogConnection // 93 + // is some kind of pool // 94 + waitUntilCaughtUp: function () { // 95 + var self = this; // 96 + if (self._stopped) // 97 + throw new Error("Called waitUntilCaughtUp on stopped handle!"); // 98 + // 99 + // Calling waitUntilCaughtUp requries us to wait for the oplog connection to // 100 + // be ready. // 101 + self._readyFuture.wait(); // 102 + // 103 + while (!self._stopped) { // 104 + // We need to make the selector at least as restrictive as the actual // 105 + // tailing selector (ie, we need to specify the DB name) or else we might // 106 + // find a TS that won't show up in the actual tail stream. // 107 + try { // 108 + var lastEntry = self._oplogLastEntryConnection.findOne( // 109 + OPLOG_COLLECTION, self._baseOplogSelector, // 110 + {fields: {ts: 1}, sort: {$natural: -1}}); // 111 + break; // 112 + } catch (e) { // 113 + // During failover (eg) if we get an exception we should log and retry // 114 + // instead of crashing. // 115 + Meteor._debug("Got exception while reading last entry: " + e); // 116 + Meteor._sleepForMs(100); // 117 + } // 118 + } // 119 + // 120 + if (self._stopped) // 121 + return; // 122 + // 123 + if (!lastEntry) { // 124 + // Really, nothing in the oplog? Well, we've processed everything. // 125 + return; // 126 + } // 127 + // 128 + var ts = lastEntry.ts; // 129 + if (!ts) // 130 + throw Error("oplog entry without ts: " + EJSON.stringify(lastEntry)); // 131 + // 132 + if (self._lastProcessedTS && ts.lessThanOrEqual(self._lastProcessedTS)) { // 133 + // We've already caught up to here. // 134 + return; // 135 + } // 136 + // 137 + // 138 + // Insert the future into our list. Almost always, this will be at the end, // 139 + // but it's conceivable that if we fail over from one primary to another, // 140 + // the oplog entries we see will go backwards. // 141 + var insertAfter = self._catchingUpFutures.length; // 142 + while (insertAfter - 1 > 0 // 143 + && self._catchingUpFutures[insertAfter - 1].ts.greaterThan(ts)) { // 144 + insertAfter--; // 145 + } // 146 + var f = new Future; // 147 + self._catchingUpFutures.splice(insertAfter, 0, {ts: ts, future: f}); // 148 + f.wait(); // 149 + }, // 150 + _startTailing: function () { // 151 + var self = this; // 152 + // We make two separate connections to Mongo. The Node Mongo driver // 153 + // implements a naive round-robin connection pool: each "connection" is a // 154 + // pool of several (5 by default) TCP connections, and each request is // 155 + // rotated through the pools. Tailable cursor queries block on the server // 156 + // until there is some data to return (or until a few seconds have // 157 + // passed). So if the connection pool used for tailing cursors is the same // 158 + // pool used for other queries, the other queries will be delayed by seconds // 159 + // 1/5 of the time. // 160 + // // 161 + // The tail connection will only ever be running a single tail command, so // 162 + // it only needs to make one underlying TCP connection. // 163 + self._oplogTailConnection = new MongoConnection( // 164 + self._oplogUrl, {poolSize: 1}); // 165 + // XXX better docs, but: it's to get monotonic results // 166 + // XXX is it safe to say "if there's an in flight query, just use its // 167 + // results"? I don't think so but should consider that // 168 + self._oplogLastEntryConnection = new MongoConnection( // 169 + self._oplogUrl, {poolSize: 1}); // 170 + // 171 + // First, make sure that there actually is a repl set here. If not, oplog // 172 + // tailing won't ever find anything! (Blocks until the connection is ready.) // 173 + var replSetInfo = self._oplogLastEntryConnection.findOne( // 174 + REPLSET_COLLECTION, {}); // 175 + if (!replSetInfo) // 176 + throw Error("$MONGO_OPLOG_URL must be set to the 'local' database of " + // 177 + "a Mongo replica set"); // 178 + // 179 + // Find the last oplog entry. // 180 + var lastOplogEntry = self._oplogLastEntryConnection.findOne( // 181 + OPLOG_COLLECTION, {}, {sort: {$natural: -1}, fields: {ts: 1}}); // 182 + // 183 + var oplogSelector = _.clone(self._baseOplogSelector); // 184 + if (lastOplogEntry) { // 185 + // Start after the last entry that currently exists. // 186 + oplogSelector.ts = {$gt: lastOplogEntry.ts}; // 187 + // If there are any calls to callWhenProcessedLatest before any other // 188 + // oplog entries show up, allow callWhenProcessedLatest to call its // 189 + // callback immediately. // 190 + self._lastProcessedTS = lastOplogEntry.ts; // 191 + } // 192 + // 193 + var cursorDescription = new CursorDescription( // 194 + OPLOG_COLLECTION, oplogSelector, {tailable: true}); // 195 + // 196 + self._tailHandle = self._oplogTailConnection.tail( // 197 + cursorDescription, function (doc) { // 198 + if (!(doc.ns && doc.ns.length > self._dbName.length + 1 && // 199 + doc.ns.substr(0, self._dbName.length + 1) === // 200 + (self._dbName + '.'))) { // 201 + throw new Error("Unexpected ns"); // 202 + } // 203 + // 204 + var trigger = {collection: doc.ns.substr(self._dbName.length + 1), // 205 + dropCollection: false, // 206 + op: doc}; // 207 + // 208 + // Is it a special command and the collection name is hidden somewhere // 209 + // in operator? // 210 + if (trigger.collection === "$cmd") { // 211 + trigger.collection = doc.o.drop; // 212 + trigger.dropCollection = true; // 213 + trigger.id = null; // 214 + } else { // 215 + // All other ops have an id. // 216 + trigger.id = idForOp(doc); // 217 + } // 218 + // 219 + self._crossbar.fire(trigger); // 220 + // 221 + // Now that we've processed this operation, process pending sequencers. // 222 + if (!doc.ts) // 223 + throw Error("oplog entry without ts: " + EJSON.stringify(doc)); // 224 + self._lastProcessedTS = doc.ts; // 225 + while (!_.isEmpty(self._catchingUpFutures) // 226 + && self._catchingUpFutures[0].ts.lessThanOrEqual( // 227 + self._lastProcessedTS)) { // 228 + var sequencer = self._catchingUpFutures.shift(); // 229 + sequencer.future.return(); // 230 + } // 231 + }); // 232 + self._readyFuture.return(); // 233 + } // 234 +}); // 235 + // 236 +///////////////////////////////////////////////////////////////////////////////////////////////////////// + +}).call(this); + + + + + + +(function () { + +///////////////////////////////////////////////////////////////////////////////////////////////////////// +// // +// packages/mongo-livedata/observe_multiplex.js // +// // +///////////////////////////////////////////////////////////////////////////////////////////////////////// + // +var Future = Npm.require('fibers/future'); // 1 + // 2 +ObserveMultiplexer = function (options) { // 3 + var self = this; // 4 + // 5 + if (!options || !_.has(options, 'ordered')) // 6 + throw Error("must specified ordered"); // 7 + // 8 + Package.facts && Package.facts.Facts.incrementServerFact( // 9 + "mongo-livedata", "observe-multiplexers", 1); // 10 + // 11 + self._ordered = options.ordered; // 12 + self._onStop = options.onStop || function () {}; // 13 + self._queue = new Meteor._SynchronousQueue(); // 14 + self._handles = {}; // 15 + self._readyFuture = new Future; // 16 + self._cache = new LocalCollection._CachingChangeObserver({ // 17 + ordered: options.ordered}); // 18 + // Number of addHandleAndSendInitialAdds tasks scheduled but not yet // 19 + // running. removeHandle uses this to know if it's time to call the onStop // 20 + // callback. // 21 + self._addHandleTasksScheduledButNotPerformed = 0; // 22 + // 23 + _.each(self.callbackNames(), function (callbackName) { // 24 + self[callbackName] = function (/* ... */) { // 25 + self._applyCallback(callbackName, _.toArray(arguments)); // 26 + }; // 27 + }); // 28 +}; // 29 + // 30 +_.extend(ObserveMultiplexer.prototype, { // 31 + addHandleAndSendInitialAdds: function (handle) { // 32 + var self = this; // 33 + // 34 + // Check this before calling runTask (even though runTask does the same // 35 + // check) so that we don't leak an ObserveMultiplexer on error by // 36 + // incrementing _addHandleTasksScheduledButNotPerformed and never // 37 + // decrementing it. // 38 + if (!self._queue.safeToRunTask()) // 39 + throw new Error( // 40 + "Can't call observeChanges from an observe callback on the same query"); // 41 + ++self._addHandleTasksScheduledButNotPerformed; // 42 + // 43 + Package.facts && Package.facts.Facts.incrementServerFact( // 44 + "mongo-livedata", "observe-handles", 1); // 45 + // 46 + self._queue.runTask(function () { // 47 + self._handles[handle._id] = handle; // 48 + // Send out whatever adds we have so far (whether or not we the // 49 + // multiplexer is ready). // 50 + self._sendAdds(handle); // 51 + --self._addHandleTasksScheduledButNotPerformed; // 52 + }); // 53 + // *outside* the task, since otherwise we'd deadlock // 54 + self._readyFuture.wait(); // 55 + }, // 56 + // 57 + // Remove an observe handle. If it was the last observe handle, call the // 58 + // onStop callback; you cannot add any more observe handles after this. // 59 + // // 60 + // This is not synchronized with polls and handle additions: this means that // 61 + // you can safely call it from within an observe callback, but it also means // 62 + // that we have to be careful when we iterate over _handles. // 63 + removeHandle: function (id) { // 64 + var self = this; // 65 + // 66 + // This should not be possible: you can only call removeHandle by having // 67 + // access to the ObserveHandle, which isn't returned to user code until the // 68 + // multiplex is ready. // 69 + if (!self._ready()) // 70 + throw new Error("Can't remove handles until the multiplex is ready"); // 71 + // 72 + delete self._handles[id]; // 73 + // 74 + Package.facts && Package.facts.Facts.incrementServerFact( // 75 + "mongo-livedata", "observe-handles", -1); // 76 + // 77 + if (_.isEmpty(self._handles) && // 78 + self._addHandleTasksScheduledButNotPerformed === 0) { // 79 + self._stop(); // 80 + } // 81 + }, // 82 + _stop: function () { // 83 + var self = this; // 84 + // It shouldn't be possible for us to stop when all our handles still // 85 + // haven't been returned from observeChanges! // 86 + if (!self._ready()) // 87 + throw Error("surprising _stop: not ready"); // 88 + // 89 + // Call stop callback (which kills the underlying process which sends us // 90 + // callbacks and removes us from the connection's dictionary). // 91 + self._onStop(); // 92 + Package.facts && Package.facts.Facts.incrementServerFact( // 93 + "mongo-livedata", "observe-multiplexers", -1); // 94 + // 95 + // Cause future addHandleAndSendInitialAdds calls to throw (but the onStop // 96 + // callback should make our connection forget about us). // 97 + self._handles = null; // 98 + }, // 99 + // Allows all addHandleAndSendInitialAdds calls to return, once all preceding // 100 + // adds have been processed. Does not block. // 101 + ready: function () { // 102 + var self = this; // 103 + self._queue.queueTask(function () { // 104 + if (self._ready()) // 105 + throw Error("can't make ObserveMultiplex ready twice!"); // 106 + self._readyFuture.return(); // 107 + }); // 108 + }, // 109 + // Calls "cb" once the effects of all "ready", "addHandleAndSendInitialAdds" // 110 + // and observe callbacks which came before this call have been propagated to // 111 + // all handles. "ready" must have already been called on this multiplexer. // 112 + onFlush: function (cb) { // 113 + var self = this; // 114 + self._queue.queueTask(function () { // 115 + if (!self._ready()) // 116 + throw Error("only call onFlush on a multiplexer that will be ready"); // 117 + cb(); // 118 + }); // 119 + }, // 120 + callbackNames: function () { // 121 + var self = this; // 122 + if (self._ordered) // 123 + return ["addedBefore", "changed", "movedBefore", "removed"]; // 124 + else // 125 + return ["added", "changed", "removed"]; // 126 + }, // 127 + _ready: function () { // 128 + return this._readyFuture.isResolved(); // 129 + }, // 130 + _applyCallback: function (callbackName, args) { // 131 + var self = this; // 132 + self._queue.queueTask(function () { // 133 + // If we stopped in the meantime, do nothing. // 134 + if (!self._handles) // 135 + return; // 136 + // 137 + // First, apply the change to the cache. // 138 + // XXX We could make applyChange callbacks promise not to hang on to any // 139 + // state from their arguments (assuming that their supplied callbacks // 140 + // don't) and skip this clone. Currently 'changed' hangs on to state // 141 + // though. // 142 + self._cache.applyChange[callbackName].apply(null, EJSON.clone(args)); // 143 + // 144 + // If we haven't finished the initial adds, then we should only be getting // 145 + // adds. // 146 + if (!self._ready() && // 147 + (callbackName !== 'added' && callbackName !== 'addedBefore')) { // 148 + throw new Error("Got " + callbackName + " during initial adds"); // 149 + } // 150 + // 151 + // Now multiplex the callbacks out to all observe handles. It's OK if // 152 + // these calls yield; since we're inside a task, no other use of our queue // 153 + // can continue until these are done. (But we do have to be careful to not // 154 + // use a handle that got removed, because removeHandle does not use the // 155 + // queue; thus, we iterate over an array of keys that we control.) // 156 + _.each(_.keys(self._handles), function (handleId) { // 157 + var handle = self._handles && self._handles[handleId]; // 158 + if (!handle) // 159 + return; // 160 + var callback = handle['_' + callbackName]; // 161 + // clone arguments so that callbacks can mutate their arguments // 162 + callback && callback.apply(null, EJSON.clone(args)); // 163 + }); // 164 + }); // 165 + }, // 166 + // 167 + // Sends initial adds to a handle. It should only be called from within a task // 168 + // (the task that is processing the addHandleAndSendInitialAdds call). It // 169 + // synchronously invokes the handle's added or addedBefore; there's no need to // 170 + // flush the queue afterwards to ensure that the callbacks get out. // 171 + _sendAdds: function (handle) { // 172 + var self = this; // 173 + if (self._queue.safeToRunTask()) // 174 + throw Error("_sendAdds may only be called from within a task!"); // 175 + var add = self._ordered ? handle._addedBefore : handle._added; // 176 + if (!add) // 177 + return; // 178 + // note: docs may be an _IdMap or an OrderedDict // 179 + self._cache.docs.forEach(function (doc, id) { // 180 + if (!_.has(self._handles, handle._id)) // 181 + throw Error("handle got removed before sending initial adds!"); // 182 + var fields = EJSON.clone(doc); // 183 + delete fields._id; // 184 + if (self._ordered) // 185 + add(id, fields, null); // we're going in order, so add at end // 186 + else // 187 + add(id, fields); // 188 + }); // 189 + } // 190 +}); // 191 + // 192 + // 193 +var nextObserveHandleId = 1; // 194 +ObserveHandle = function (multiplexer, callbacks) { // 195 + var self = this; // 196 + // The end user is only supposed to call stop(). The other fields are // 197 + // accessible to the multiplexer, though. // 198 + self._multiplexer = multiplexer; // 199 + _.each(multiplexer.callbackNames(), function (name) { // 200 + if (callbacks[name]) { // 201 + self['_' + name] = callbacks[name]; // 202 + } else if (name === "addedBefore" && callbacks.added) { // 203 + // Special case: if you specify "added" and "movedBefore", you get an // 204 + // ordered observe where for some reason you don't get ordering data on // 205 + // the adds. I dunno, we wrote tests for it, there must have been a // 206 + // reason. // 207 + self._addedBefore = function (id, fields, before) { // 208 + callbacks.added(id, fields); // 209 + }; // 210 + } // 211 + }); // 212 + self._stopped = false; // 213 + self._id = nextObserveHandleId++; // 214 +}; // 215 +ObserveHandle.prototype.stop = function () { // 216 + var self = this; // 217 + if (self._stopped) // 218 + return; // 219 + self._stopped = true; // 220 + self._multiplexer.removeHandle(self._id); // 221 +}; // 222 + // 223 +///////////////////////////////////////////////////////////////////////////////////////////////////////// + +}).call(this); + + + + + + +(function () { + +///////////////////////////////////////////////////////////////////////////////////////////////////////// +// // +// packages/mongo-livedata/doc_fetcher.js // +// // +///////////////////////////////////////////////////////////////////////////////////////////////////////// + // +var Fiber = Npm.require('fibers'); // 1 +var Future = Npm.require('fibers/future'); // 2 + // 3 +DocFetcher = function (mongoConnection) { // 4 + var self = this; // 5 + self._mongoConnection = mongoConnection; // 6 + // Map from cache key -> [callback] // 7 + self._callbacksForCacheKey = {}; // 8 +}; // 9 + // 10 +_.extend(DocFetcher.prototype, { // 11 + // Fetches document "id" from collectionName, returning it or null if not // 12 + // found. // 13 + // // 14 + // If you make multiple calls to fetch() with the same cacheKey (a string), // 15 + // DocFetcher may assume that they all return the same document. (It does // 16 + // not check to see if collectionName/id match.) // 17 + // // 18 + // You may assume that callback is never called synchronously (and in fact // 19 + // OplogObserveDriver does so). // 20 + fetch: function (collectionName, id, cacheKey, callback) { // 21 + var self = this; // 22 + // 23 + check(collectionName, String); // 24 + // id is some sort of scalar // 25 + check(cacheKey, String); // 26 + // 27 + // If there's already an in-progress fetch for this cache key, yield until // 28 + // it's done and return whatever it returns. // 29 + if (_.has(self._callbacksForCacheKey, cacheKey)) { // 30 + self._callbacksForCacheKey[cacheKey].push(callback); // 31 + return; // 32 + } // 33 + // 34 + var callbacks = self._callbacksForCacheKey[cacheKey] = [callback]; // 35 + // 36 + Fiber(function () { // 37 + try { // 38 + var doc = self._mongoConnection.findOne( // 39 + collectionName, {_id: id}) || null; // 40 + // Return doc to all relevant callbacks. Note that this array can // 41 + // continue to grow during callback excecution. // 42 + while (!_.isEmpty(callbacks)) { // 43 + // Clone the document so that the various calls to fetch don't return // 44 + // objects that are intertwingled with each other. Clone before // 45 + // popping the future, so that if clone throws, the error gets passed // 46 + // to the next callback. // 47 + var clonedDoc = EJSON.clone(doc); // 48 + callbacks.pop()(null, clonedDoc); // 49 + } // 50 + } catch (e) { // 51 + while (!_.isEmpty(callbacks)) { // 52 + callbacks.pop()(e); // 53 + } // 54 + } finally { // 55 + // XXX consider keeping the doc around for a period of time before // 56 + // removing from the cache // 57 + delete self._callbacksForCacheKey[cacheKey]; // 58 + } // 59 + }).run(); // 60 + } // 61 +}); // 62 + // 63 +MongoTest.DocFetcher = DocFetcher; // 64 + // 65 +///////////////////////////////////////////////////////////////////////////////////////////////////////// + +}).call(this); + + + + + + +(function () { + +///////////////////////////////////////////////////////////////////////////////////////////////////////// +// // +// packages/mongo-livedata/polling_observe_driver.js // +// // +///////////////////////////////////////////////////////////////////////////////////////////////////////// + // +PollingObserveDriver = function (options) { // 1 + var self = this; // 2 + // 3 + self._cursorDescription = options.cursorDescription; // 4 + self._mongoHandle = options.mongoHandle; // 5 + self._ordered = options.ordered; // 6 + self._multiplexer = options.multiplexer; // 7 + self._stopCallbacks = []; // 8 + self._stopped = false; // 9 + // 10 + self._synchronousCursor = self._mongoHandle._createSynchronousCursor( // 11 + self._cursorDescription); // 12 + // 13 + // previous results snapshot. on each poll cycle, diffs against // 14 + // results drives the callbacks. // 15 + self._results = null; // 16 + // 17 + // The number of _pollMongo calls that have been added to self._taskQueue but // 18 + // have not started running. Used to make sure we never schedule more than one // 19 + // _pollMongo (other than possibly the one that is currently running). It's // 20 + // also used by _suspendPolling to pretend there's a poll scheduled. Usually, // 21 + // it's either 0 (for "no polls scheduled other than maybe one currently // 22 + // running") or 1 (for "a poll scheduled that isn't running yet"), but it can // 23 + // also be 2 if incremented by _suspendPolling. // 24 + self._pollsScheduledButNotStarted = 0; // 25 + self._pendingWrites = []; // people to notify when polling completes // 26 + // 27 + // Make sure to create a separately throttled function for each // 28 + // PollingObserveDriver object. // 29 + self._ensurePollIsScheduled = _.throttle( // 30 + self._unthrottledEnsurePollIsScheduled, 50 /* ms */); // 31 + // 32 + // XXX figure out if we still need a queue // 33 + self._taskQueue = new Meteor._SynchronousQueue(); // 34 + // 35 + var listenersHandle = listenAll( // 36 + self._cursorDescription, function (notification) { // 37 + // When someone does a transaction that might affect us, schedule a poll // 38 + // of the database. If that transaction happens inside of a write fence, // 39 + // block the fence until we've polled and notified observers. // 40 + var fence = DDPServer._CurrentWriteFence.get(); // 41 + if (fence) // 42 + self._pendingWrites.push(fence.beginWrite()); // 43 + // Ensure a poll is scheduled... but if we already know that one is, // 44 + // don't hit the throttled _ensurePollIsScheduled function (which might // 45 + // lead to us calling it unnecessarily in 50ms). // 46 + if (self._pollsScheduledButNotStarted === 0) // 47 + self._ensurePollIsScheduled(); // 48 + } // 49 + ); // 50 + self._stopCallbacks.push(function () { listenersHandle.stop(); }); // 51 + // 52 + // every once and a while, poll even if we don't think we're dirty, for // 53 + // eventual consistency with database writes from outside the Meteor // 54 + // universe. // 55 + // // 56 + // For testing, there's an undocumented callback argument to observeChanges // 57 + // which disables time-based polling and gets called at the beginning of each // 58 + // poll. // 59 + if (options._testOnlyPollCallback) { // 60 + self._testOnlyPollCallback = options._testOnlyPollCallback; // 61 + } else { // 62 + var intervalHandle = Meteor.setInterval( // 63 + _.bind(self._ensurePollIsScheduled, self), 10 * 1000); // 64 + self._stopCallbacks.push(function () { // 65 + Meteor.clearInterval(intervalHandle); // 66 + }); // 67 + } // 68 + // 69 + // Make sure we actually poll soon! // 70 + self._unthrottledEnsurePollIsScheduled(); // 71 + // 72 + Package.facts && Package.facts.Facts.incrementServerFact( // 73 + "mongo-livedata", "observe-drivers-polling", 1); // 74 +}; // 75 + // 76 +_.extend(PollingObserveDriver.prototype, { // 77 + // This is always called through _.throttle (except once at startup). // 78 + _unthrottledEnsurePollIsScheduled: function () { // 79 + var self = this; // 80 + if (self._pollsScheduledButNotStarted > 0) // 81 + return; // 82 + ++self._pollsScheduledButNotStarted; // 83 + self._taskQueue.queueTask(function () { // 84 + self._pollMongo(); // 85 + }); // 86 + }, // 87 + // 88 + // test-only interface for controlling polling. // 89 + // // 90 + // _suspendPolling blocks until any currently running and scheduled polls are // 91 + // done, and prevents any further polls from being scheduled. (new // 92 + // ObserveHandles can be added and receive their initial added callbacks, // 93 + // though.) // 94 + // // 95 + // _resumePolling immediately polls, and allows further polls to occur. // 96 + _suspendPolling: function() { // 97 + var self = this; // 98 + // Pretend that there's another poll scheduled (which will prevent // 99 + // _ensurePollIsScheduled from queueing any more polls). // 100 + ++self._pollsScheduledButNotStarted; // 101 + // Now block until all currently running or scheduled polls are done. // 102 + self._taskQueue.runTask(function() {}); // 103 + // 104 + // Confirm that there is only one "poll" (the fake one we're pretending to // 105 + // have) scheduled. // 106 + if (self._pollsScheduledButNotStarted !== 1) // 107 + throw new Error("_pollsScheduledButNotStarted is " + // 108 + self._pollsScheduledButNotStarted); // 109 + }, // 110 + _resumePolling: function() { // 111 + var self = this; // 112 + // We should be in the same state as in the end of _suspendPolling. // 113 + if (self._pollsScheduledButNotStarted !== 1) // 114 + throw new Error("_pollsScheduledButNotStarted is " + // 115 + self._pollsScheduledButNotStarted); // 116 + // Run a poll synchronously (which will counteract the // 117 + // ++_pollsScheduledButNotStarted from _suspendPolling). // 118 + self._taskQueue.runTask(function () { // 119 + self._pollMongo(); // 120 + }); // 121 + }, // 122 + // 123 + _pollMongo: function () { // 124 + var self = this; // 125 + --self._pollsScheduledButNotStarted; // 126 + // 127 + var first = false; // 128 + var oldResults = self._results; // 129 + if (!oldResults) { // 130 + first = true; // 131 + // XXX maybe use OrderedDict instead? // 132 + oldResults = self._ordered ? [] : new LocalCollection._IdMap; // 133 + } // 134 + // 135 + self._testOnlyPollCallback && self._testOnlyPollCallback(); // 136 + // 137 + // Save the list of pending writes which this round will commit. // 138 + var writesForCycle = self._pendingWrites; // 139 + self._pendingWrites = []; // 140 + // 141 + // Get the new query results. (This yields.) // 142 + try { // 143 + var newResults = self._synchronousCursor.getRawObjects(self._ordered); // 144 + } catch (e) { // 145 + // getRawObjects can throw if we're having trouble talking to the // 146 + // database. That's fine --- we will repoll later anyway. But we should // 147 + // make sure not to lose track of this cycle's writes. // 148 + Array.prototype.push.apply(self._pendingWrites, writesForCycle); // 149 + throw e; // 150 + } // 151 + // 152 + // Run diffs. // 153 + if (!self._stopped) { // 154 + LocalCollection._diffQueryChanges( // 155 + self._ordered, oldResults, newResults, self._multiplexer); // 156 + } // 157 + // 158 + // Signals the multiplexer to allow all observeChanges calls that share this // 159 + // multiplexer to return. (This happens asynchronously, via the // 160 + // multiplexer's queue.) // 161 + if (first) // 162 + self._multiplexer.ready(); // 163 + // 164 + // Replace self._results atomically. (This assignment is what makes `first` // 165 + // stay through on the next cycle, so we've waited until after we've // 166 + // committed to ready-ing the multiplexer.) // 167 + self._results = newResults; // 168 + // 169 + // Once the ObserveMultiplexer has processed everything we've done in this // 170 + // round, mark all the writes which existed before this call as // 171 + // commmitted. (If new writes have shown up in the meantime, there'll // 172 + // already be another _pollMongo task scheduled.) // 173 + self._multiplexer.onFlush(function () { // 174 + _.each(writesForCycle, function (w) { // 175 + w.committed(); // 176 + }); // 177 + }); // 178 + }, // 179 + // 180 + stop: function () { // 181 + var self = this; // 182 + self._stopped = true; // 183 + _.each(self._stopCallbacks, function (c) { c(); }); // 184 + Package.facts && Package.facts.Facts.incrementServerFact( // 185 + "mongo-livedata", "observe-drivers-polling", -1); // 186 + } // 187 +}); // 188 + // 189 +///////////////////////////////////////////////////////////////////////////////////////////////////////// + +}).call(this); + + + + + + +(function () { + +///////////////////////////////////////////////////////////////////////////////////////////////////////// +// // +// packages/mongo-livedata/oplog_observe_driver.js // +// // +///////////////////////////////////////////////////////////////////////////////////////////////////////// + // +var Fiber = Npm.require('fibers'); // 1 +var Future = Npm.require('fibers/future'); // 2 + // 3 +var PHASE = { // 4 + QUERYING: "QUERYING", // 5 + FETCHING: "FETCHING", // 6 + STEADY: "STEADY" // 7 +}; // 8 + // 9 +// Exception thrown by _needToPollQuery which unrolls the stack up to the // 10 +// enclosing call to finishIfNeedToPollQuery. // 11 +var SwitchedToQuery = function () {}; // 12 +var finishIfNeedToPollQuery = function (f) { // 13 + return function () { // 14 + try { // 15 + f.apply(this, arguments); // 16 + } catch (e) { // 17 + if (!(e instanceof SwitchedToQuery)) // 18 + throw e; // 19 + } // 20 + }; // 21 +}; // 22 + // 23 +// OplogObserveDriver is an alternative to PollingObserveDriver which follows // 24 +// the Mongo operation log instead of just re-polling the query. It obeys the // 25 +// same simple interface: constructing it starts sending observeChanges // 26 +// callbacks (and a ready() invocation) to the ObserveMultiplexer, and you stop // 27 +// it by calling the stop() method. // 28 +OplogObserveDriver = function (options) { // 29 + var self = this; // 30 + self._usesOplog = true; // tests look at this // 31 + // 32 + self._cursorDescription = options.cursorDescription; // 33 + self._mongoHandle = options.mongoHandle; // 34 + self._multiplexer = options.multiplexer; // 35 + // 36 + if (options.ordered) { // 37 + throw Error("OplogObserveDriver only supports unordered observeChanges"); // 38 + } // 39 + // 40 + var sorter = options.sorter; // 41 + // We don't support $near and other geo-queries so it's OK to initialize the // 42 + // comparator only once in the constructor. // 43 + var comparator = sorter && sorter.getComparator(); // 44 + // 45 + if (options.cursorDescription.options.limit) { // 46 + // There are several properties ordered driver implements: // 47 + // - _limit is a positive number // 48 + // - _comparator is a function-comparator by which the query is ordered // 49 + // - _unpublishedBuffer is non-null Min/Max Heap, // 50 + // the empty buffer in STEADY phase implies that the // 51 + // everything that matches the queries selector fits // 52 + // into published set. // 53 + // - _published - Min Heap (also implements IdMap methods) // 54 + // 55 + var heapOptions = { IdMap: LocalCollection._IdMap }; // 56 + self._limit = self._cursorDescription.options.limit; // 57 + self._comparator = comparator; // 58 + self._sorter = sorter; // 59 + self._unpublishedBuffer = new MinMaxHeap(comparator, heapOptions); // 60 + // We need something that can find Max value in addition to IdMap interface // 61 + self._published = new MaxHeap(comparator, heapOptions); // 62 + } else { // 63 + self._limit = 0; // 64 + self._comparator = null; // 65 + self._sorter = null; // 66 + self._unpublishedBuffer = null; // 67 + self._published = new LocalCollection._IdMap; // 68 + } // 69 + // 70 + // Indicates if it is safe to insert a new document at the end of the buffer // 71 + // for this query. i.e. it is known that there are no documents matching the // 72 + // selector those are not in published or buffer. // 73 + self._safeAppendToBuffer = false; // 74 + // 75 + self._stopped = false; // 76 + self._stopHandles = []; // 77 + // 78 + Package.facts && Package.facts.Facts.incrementServerFact( // 79 + "mongo-livedata", "observe-drivers-oplog", 1); // 80 + // 81 + self._registerPhaseChange(PHASE.QUERYING); // 82 + // 83 + var selector = self._cursorDescription.selector; // 84 + self._matcher = options.matcher; // 85 + var projection = self._cursorDescription.options.fields || {}; // 86 + self._projectionFn = LocalCollection._compileProjection(projection); // 87 + // Projection function, result of combining important fields for selector and // 88 + // existing fields projection // 89 + self._sharedProjection = self._matcher.combineIntoProjection(projection); // 90 + if (sorter) // 91 + self._sharedProjection = sorter.combineIntoProjection(self._sharedProjection); // 92 + self._sharedProjectionFn = LocalCollection._compileProjection( // 93 + self._sharedProjection); // 94 + // 95 + self._needToFetch = new LocalCollection._IdMap; // 96 + self._currentlyFetching = null; // 97 + self._fetchGeneration = 0; // 98 + // 99 + self._requeryWhenDoneThisQuery = false; // 100 + self._writesToCommitWhenWeReachSteady = []; // 101 + // 102 + forEachTrigger(self._cursorDescription, function (trigger) { // 103 + self._stopHandles.push(self._mongoHandle._oplogHandle.onOplogEntry( // 104 + trigger, function (notification) { // 105 + Meteor._noYieldsAllowed(finishIfNeedToPollQuery(function () { // 106 + var op = notification.op; // 107 + if (notification.dropCollection) { // 108 + // Note: this call is not allowed to block on anything (especially // 109 + // on waiting for oplog entries to catch up) because that will block // 110 + // onOplogEntry! // 111 + self._needToPollQuery(); // 112 + } else { // 113 + // All other operators should be handled depending on phase // 114 + if (self._phase === PHASE.QUERYING) // 115 + self._handleOplogEntryQuerying(op); // 116 + else // 117 + self._handleOplogEntrySteadyOrFetching(op); // 118 + } // 119 + })); // 120 + } // 121 + )); // 122 + }); // 123 + // 124 + // XXX ordering w.r.t. everything else? // 125 + self._stopHandles.push(listenAll( // 126 + self._cursorDescription, function (notification) { // 127 + // If we're not in a write fence, we don't have to do anything. // 128 + var fence = DDPServer._CurrentWriteFence.get(); // 129 + if (!fence) // 130 + return; // 131 + var write = fence.beginWrite(); // 132 + // This write cannot complete until we've caught up to "this point" in the // 133 + // oplog, and then made it back to the steady state. // 134 + Meteor.defer(function () { // 135 + self._mongoHandle._oplogHandle.waitUntilCaughtUp(); // 136 + if (self._stopped) { // 137 + // We're stopped, so just immediately commit. // 138 + write.committed(); // 139 + } else if (self._phase === PHASE.STEADY) { // 140 + // Make sure that all of the callbacks have made it through the // 141 + // multiplexer and been delivered to ObserveHandles before committing // 142 + // writes. // 143 + self._multiplexer.onFlush(function () { // 144 + write.committed(); // 145 + }); // 146 + } else { // 147 + self._writesToCommitWhenWeReachSteady.push(write); // 148 + } // 149 + }); // 150 + } // 151 + )); // 152 + // 153 + // When Mongo fails over, we need to repoll the query, in case we processed an // 154 + // oplog entry that got rolled back. // 155 + self._stopHandles.push(self._mongoHandle._onFailover(finishIfNeedToPollQuery( // 156 + function () { // 157 + self._needToPollQuery(); // 158 + }))); // 159 + // 160 + // Give _observeChanges a chance to add the new ObserveHandle to our // 161 + // multiplexer, so that the added calls get streamed. // 162 + Meteor.defer(finishIfNeedToPollQuery(function () { // 163 + self._runInitialQuery(); // 164 + })); // 165 +}; // 166 + // 167 +_.extend(OplogObserveDriver.prototype, { // 168 + _addPublished: function (id, doc) { // 169 + var self = this; // 170 + var fields = _.clone(doc); // 171 + delete fields._id; // 172 + self._published.set(id, self._sharedProjectionFn(doc)); // 173 + self._multiplexer.added(id, self._projectionFn(fields)); // 174 + // 175 + // After adding this document, the published set might be overflowed // 176 + // (exceeding capacity specified by limit). If so, push the maximum element // 177 + // to the buffer, we might want to save it in memory to reduce the amount of // 178 + // Mongo lookups in the future. // 179 + if (self._limit && self._published.size() > self._limit) { // 180 + // XXX in theory the size of published is no more than limit+1 // 181 + if (self._published.size() !== self._limit + 1) { // 182 + throw new Error("After adding to published, " + // 183 + (self._published.size() - self._limit) + // 184 + " documents are overflowing the set"); // 185 + } // 186 + // 187 + var overflowingDocId = self._published.maxElementId(); // 188 + var overflowingDoc = self._published.get(overflowingDocId); // 189 + // 190 + if (EJSON.equals(overflowingDocId, id)) { // 191 + throw new Error("The document just added is overflowing the published set"); // 192 + } // 193 + // 194 + self._published.remove(overflowingDocId); // 195 + self._multiplexer.removed(overflowingDocId); // 196 + self._addBuffered(overflowingDocId, overflowingDoc); // 197 + } // 198 + }, // 199 + _removePublished: function (id) { // 200 + var self = this; // 201 + self._published.remove(id); // 202 + self._multiplexer.removed(id); // 203 + if (! self._limit || self._published.size() === self._limit) // 204 + return; // 205 + // 206 + if (self._published.size() > self._limit) // 207 + throw Error("self._published got too big"); // 208 + // 209 + // OK, we are publishing less than the limit. Maybe we should look in the // 210 + // buffer to find the next element past what we were publishing before. // 211 + // 212 + if (!self._unpublishedBuffer.empty()) { // 213 + // There's something in the buffer; move the first thing in it to // 214 + // _published. // 215 + var newDocId = self._unpublishedBuffer.minElementId(); // 216 + var newDoc = self._unpublishedBuffer.get(newDocId); // 217 + self._removeBuffered(newDocId); // 218 + self._addPublished(newDocId, newDoc); // 219 + return; // 220 + } // 221 + // 222 + // There's nothing in the buffer. This could mean one of a few things. // 223 + // 224 + // (a) We could be in the middle of re-running the query (specifically, we // 225 + // could be in _publishNewResults). In that case, _unpublishedBuffer is // 226 + // empty because we clear it at the beginning of _publishNewResults. In this // 227 + // case, our caller already knows the entire answer to the query and we // 228 + // don't need to do anything fancy here. Just return. // 229 + if (self._phase === PHASE.QUERYING) // 230 + return; // 231 + // 232 + // (b) We're pretty confident that the union of _published and // 233 + // _unpublishedBuffer contain all documents that match selector. Because // 234 + // _unpublishedBuffer is empty, that means we're confident that _published // 235 + // contains all documents that match selector. So we have nothing to do. // 236 + if (self._safeAppendToBuffer) // 237 + return; // 238 + // 239 + // (c) Maybe there are other documents out there that should be in our // 240 + // buffer. But in that case, when we emptied _unpublishedBuffer in // 241 + // _removeBuffered, we should have called _needToPollQuery, which will // 242 + // either put something in _unpublishedBuffer or set _safeAppendToBuffer (or // 243 + // both), and it will put us in QUERYING for that whole time. So in fact, we // 244 + // shouldn't be able to get here. // 245 + // 246 + throw new Error("Buffer inexplicably empty"); // 247 + }, // 248 + _changePublished: function (id, oldDoc, newDoc) { // 249 + var self = this; // 250 + self._published.set(id, self._sharedProjectionFn(newDoc)); // 251 + var changed = LocalCollection._makeChangedFields(_.clone(newDoc), oldDoc); // 252 + changed = self._projectionFn(changed); // 253 + if (!_.isEmpty(changed)) // 254 + self._multiplexer.changed(id, changed); // 255 + }, // 256 + _addBuffered: function (id, doc) { // 257 + var self = this; // 258 + self._unpublishedBuffer.set(id, self._sharedProjectionFn(doc)); // 259 + // 260 + // If something is overflowing the buffer, we just remove it from cache // 261 + if (self._unpublishedBuffer.size() > self._limit) { // 262 + var maxBufferedId = self._unpublishedBuffer.maxElementId(); // 263 + // 264 + self._unpublishedBuffer.remove(maxBufferedId); // 265 + // 266 + // Since something matching is removed from cache (both published set and // 267 + // buffer), set flag to false // 268 + self._safeAppendToBuffer = false; // 269 + } // 270 + }, // 271 + // Is called either to remove the doc completely from matching set or to move // 272 + // it to the published set later. // 273 + _removeBuffered: function (id) { // 274 + var self = this; // 275 + self._unpublishedBuffer.remove(id); // 276 + // To keep the contract "buffer is never empty in STEADY phase unless the // 277 + // everything matching fits into published" true, we poll everything as soon // 278 + // as we see the buffer becoming empty. // 279 + if (! self._unpublishedBuffer.size() && ! self._safeAppendToBuffer) // 280 + self._needToPollQuery(); // 281 + }, // 282 + // Called when a document has joined the "Matching" results set. // 283 + // Takes responsibility of keeping _unpublishedBuffer in sync with _published // 284 + // and the effect of limit enforced. // 285 + _addMatching: function (doc) { // 286 + var self = this; // 287 + var id = doc._id; // 288 + if (self._published.has(id)) // 289 + throw Error("tried to add something already published " + id); // 290 + if (self._limit && self._unpublishedBuffer.has(id)) // 291 + throw Error("tried to add something already existed in buffer " + id); // 292 + // 293 + var limit = self._limit; // 294 + var comparator = self._comparator; // 295 + var maxPublished = (limit && self._published.size() > 0) ? // 296 + self._published.get(self._published.maxElementId()) : null; // 297 + var maxBuffered = (limit && self._unpublishedBuffer.size() > 0) ? // 298 + self._unpublishedBuffer.get(self._unpublishedBuffer.maxElementId()) : null; // 299 + // The query is unlimited or didn't publish enough documents yet or the new // 300 + // document would fit into published set pushing the maximum element out, // 301 + // then we need to publish the doc. // 302 + var toPublish = ! limit || self._published.size() < limit || // 303 + comparator(doc, maxPublished) < 0; // 304 + // 305 + // Otherwise we might need to buffer it (only in case of limited query). // 306 + // Buffering is allowed if the buffer is not filled up yet and all matching // 307 + // docs are either in the published set or in the buffer. // 308 + var canAppendToBuffer = !toPublish && self._safeAppendToBuffer && // 309 + self._unpublishedBuffer.size() < limit; // 310 + // 311 + // Or if it is small enough to be safely inserted to the middle or the // 312 + // beginning of the buffer. // 313 + var canInsertIntoBuffer = !toPublish && maxBuffered && // 314 + comparator(doc, maxBuffered) <= 0; // 315 + // 316 + var toBuffer = canAppendToBuffer || canInsertIntoBuffer; // 317 + // 318 + if (toPublish) { // 319 + self._addPublished(id, doc); // 320 + } else if (toBuffer) { // 321 + self._addBuffered(id, doc); // 322 + } else { // 323 + // dropping it and not saving to the cache // 324 + self._safeAppendToBuffer = false; // 325 + } // 326 + }, // 327 + // Called when a document leaves the "Matching" results set. // 328 + // Takes responsibility of keeping _unpublishedBuffer in sync with _published // 329 + // and the effect of limit enforced. // 330 + _removeMatching: function (id) { // 331 + var self = this; // 332 + if (! self._published.has(id) && ! self._limit) // 333 + throw Error("tried to remove something matching but not cached " + id); // 334 + // 335 + if (self._published.has(id)) { // 336 + self._removePublished(id); // 337 + } else if (self._unpublishedBuffer.has(id)) { // 338 + self._removeBuffered(id); // 339 + } // 340 + }, // 341 + _handleDoc: function (id, newDoc) { // 342 + var self = this; // 343 + var matchesNow = newDoc && self._matcher.documentMatches(newDoc).result; // 344 + // 345 + var publishedBefore = self._published.has(id); // 346 + var bufferedBefore = self._limit && self._unpublishedBuffer.has(id); // 347 + var cachedBefore = publishedBefore || bufferedBefore; // 348 + // 349 + if (matchesNow && !cachedBefore) { // 350 + self._addMatching(newDoc); // 351 + } else if (cachedBefore && !matchesNow) { // 352 + self._removeMatching(id); // 353 + } else if (cachedBefore && matchesNow) { // 354 + var oldDoc = self._published.get(id); // 355 + var comparator = self._comparator; // 356 + var minBuffered = self._limit && self._unpublishedBuffer.size() && // 357 + self._unpublishedBuffer.get(self._unpublishedBuffer.minElementId()); // 358 + // 359 + if (publishedBefore) { // 360 + // Unlimited case where the document stays in published once it matches // 361 + // or the case when we don't have enough matching docs to publish or the // 362 + // changed but matching doc will stay in published anyways. // 363 + // XXX: We rely on the emptiness of buffer. Be sure to maintain the fact // 364 + // that buffer can't be empty if there are matching documents not // 365 + // published. Notably, we don't want to schedule repoll and continue // 366 + // relying on this property. // 367 + var staysInPublished = ! self._limit || // 368 + self._unpublishedBuffer.size() === 0 || // 369 + comparator(newDoc, minBuffered) <= 0; // 370 + // 371 + if (staysInPublished) { // 372 + self._changePublished(id, oldDoc, newDoc); // 373 + } else { // 374 + // after the change doc doesn't stay in the published, remove it // 375 + self._removePublished(id); // 376 + // but it can move into buffered now, check it // 377 + var maxBuffered = self._unpublishedBuffer.get(self._unpublishedBuffer.maxElementId()); // 378 + // 379 + var toBuffer = self._safeAppendToBuffer || // 380 + (maxBuffered && comparator(newDoc, maxBuffered) <= 0); // 381 + // 382 + if (toBuffer) { // 383 + self._addBuffered(id, newDoc); // 384 + } else { // 385 + // Throw away from both published set and buffer // 386 + self._safeAppendToBuffer = false; // 387 + } // 388 + } // 389 + } else if (bufferedBefore) { // 390 + oldDoc = self._unpublishedBuffer.get(id); // 391 + // remove the old version manually instead of using _removeBuffered so // 392 + // we don't trigger the querying immediately. if we end this block with // 393 + // the buffer empty, we will need to trigger the query poll manually // 394 + // too. // 395 + self._unpublishedBuffer.remove(id); // 396 + // 397 + var maxPublished = self._published.get(self._published.maxElementId()); // 398 + var maxBuffered = self._unpublishedBuffer.size() && self._unpublishedBuffer.get(self._unpublishedBuffer.maxElementId()); + // 400 + // the buffered doc was updated, it could move to published // 401 + var toPublish = comparator(newDoc, maxPublished) < 0; // 402 + // 403 + // or stays in buffer even after the change // 404 + var staysInBuffer = (! toPublish && self._safeAppendToBuffer) || // 405 + (!toPublish && maxBuffered && comparator(newDoc, maxBuffered) <= 0); // 406 + // 407 + if (toPublish) { // 408 + self._addPublished(id, newDoc); // 409 + } else if (staysInBuffer) { // 410 + // stays in buffer but changes // 411 + self._unpublishedBuffer.set(id, newDoc); // 412 + } else { // 413 + // Throw away from both published set and buffer // 414 + self._safeAppendToBuffer = false; // 415 + // Normally this check would have been done in _removeBuffered but we // 416 + // didn't use it, so we need to do it ourself now. // 417 + if (! self._unpublishedBuffer.size()) { // 418 + self._needToPollQuery(); // 419 + } // 420 + } // 421 + } else { // 422 + throw new Error("cachedBefore implies either of publishedBefore or bufferedBefore is true."); // 423 + } // 424 + } // 425 + }, // 426 + _fetchModifiedDocuments: function () { // 427 + var self = this; // 428 + self._registerPhaseChange(PHASE.FETCHING); // 429 + // Defer, because nothing called from the oplog entry handler may yield, but // 430 + // fetch() yields. // 431 + Meteor.defer(finishIfNeedToPollQuery(function () { // 432 + while (!self._stopped && !self._needToFetch.empty()) { // 433 + if (self._phase === PHASE.QUERYING) { // 434 + // While fetching, we decided to go into QUERYING mode, and then we // 435 + // saw another oplog entry, so _needToFetch is not empty. But we // 436 + // shouldn't fetch these documents until AFTER the query is done. // 437 + break; // 438 + } // 439 + // 440 + // Being in steady phase here would be surprising. // 441 + if (self._phase !== PHASE.FETCHING) // 442 + throw new Error("phase in fetchModifiedDocuments: " + self._phase); // 443 + // 444 + self._currentlyFetching = self._needToFetch; // 445 + var thisGeneration = ++self._fetchGeneration; // 446 + self._needToFetch = new LocalCollection._IdMap; // 447 + var waiting = 0; // 448 + var fut = new Future; // 449 + // This loop is safe, because _currentlyFetching will not be updated // 450 + // during this loop (in fact, it is never mutated). // 451 + self._currentlyFetching.forEach(function (cacheKey, id) { // 452 + waiting++; // 453 + self._mongoHandle._docFetcher.fetch( // 454 + self._cursorDescription.collectionName, id, cacheKey, // 455 + finishIfNeedToPollQuery(function (err, doc) { // 456 + try { // 457 + if (err) { // 458 + Meteor._debug("Got exception while fetching documents: " + // 459 + err); // 460 + // If we get an error from the fetcher (eg, trouble connecting // 461 + // to Mongo), let's just abandon the fetch phase altogether // 462 + // and fall back to polling. It's not like we're getting live // 463 + // updates anyway. // 464 + if (self._phase !== PHASE.QUERYING) { // 465 + self._needToPollQuery(); // 466 + } // 467 + } else if (!self._stopped && self._phase === PHASE.FETCHING // 468 + && self._fetchGeneration === thisGeneration) { // 469 + // We re-check the generation in case we've had an explicit // 470 + // _pollQuery call (eg, in another fiber) which should // 471 + // effectively cancel this round of fetches. (_pollQuery // 472 + // increments the generation.) // 473 + self._handleDoc(id, doc); // 474 + } // 475 + } finally { // 476 + waiting--; // 477 + // Because fetch() never calls its callback synchronously, this // 478 + // is safe (ie, we won't call fut.return() before the forEach is // 479 + // done). // 480 + if (waiting === 0) // 481 + fut.return(); // 482 + } // 483 + })); // 484 + }); // 485 + fut.wait(); // 486 + // Exit now if we've had a _pollQuery call (here or in another fiber). // 487 + if (self._phase === PHASE.QUERYING) // 488 + return; // 489 + self._currentlyFetching = null; // 490 + } // 491 + // We're done fetching, so we can be steady, unless we've had a _pollQuery // 492 + // call (here or in another fiber). // 493 + if (self._phase !== PHASE.QUERYING) // 494 + self._beSteady(); // 495 + })); // 496 + }, // 497 + _beSteady: function () { // 498 + var self = this; // 499 + self._registerPhaseChange(PHASE.STEADY); // 500 + var writes = self._writesToCommitWhenWeReachSteady; // 501 + self._writesToCommitWhenWeReachSteady = []; // 502 + self._multiplexer.onFlush(function () { // 503 + _.each(writes, function (w) { // 504 + w.committed(); // 505 + }); // 506 + }); // 507 + }, // 508 + _handleOplogEntryQuerying: function (op) { // 509 + var self = this; // 510 + self._needToFetch.set(idForOp(op), op.ts.toString()); // 511 + }, // 512 + _handleOplogEntrySteadyOrFetching: function (op) { // 513 + var self = this; // 514 + var id = idForOp(op); // 515 + // If we're already fetching this one, or about to, we can't optimize; make // 516 + // sure that we fetch it again if necessary. // 517 + if (self._phase === PHASE.FETCHING && // 518 + ((self._currentlyFetching && self._currentlyFetching.has(id)) || // 519 + self._needToFetch.has(id))) { // 520 + self._needToFetch.set(id, op.ts.toString()); // 521 + return; // 522 + } // 523 + // 524 + if (op.op === 'd') { // 525 + if (self._published.has(id) || (self._limit && self._unpublishedBuffer.has(id))) // 526 + self._removeMatching(id); // 527 + } else if (op.op === 'i') { // 528 + if (self._published.has(id)) // 529 + throw new Error("insert found for already-existing ID in published"); // 530 + if (self._unpublishedBuffer && self._unpublishedBuffer.has(id)) // 531 + throw new Error("insert found for already-existing ID in buffer"); // 532 + // 533 + // XXX what if selector yields? for now it can't but later it could have // 534 + // $where // 535 + if (self._matcher.documentMatches(op.o).result) // 536 + self._addMatching(op.o); // 537 + } else if (op.op === 'u') { // 538 + // Is this a modifier ($set/$unset, which may require us to poll the // 539 + // database to figure out if the whole document matches the selector) or a // 540 + // replacement (in which case we can just directly re-evaluate the // 541 + // selector)? // 542 + var isReplace = !_.has(op.o, '$set') && !_.has(op.o, '$unset'); // 543 + // If this modifier modifies something inside an EJSON custom type (ie, // 544 + // anything with EJSON$), then we can't try to use // 545 + // LocalCollection._modify, since that just mutates the EJSON encoding, // 546 + // not the actual object. // 547 + var canDirectlyModifyDoc = // 548 + !isReplace && modifierCanBeDirectlyApplied(op.o); // 549 + // 550 + var publishedBefore = self._published.has(id); // 551 + var bufferedBefore = self._limit && self._unpublishedBuffer.has(id); // 552 + // 553 + if (isReplace) { // 554 + self._handleDoc(id, _.extend({_id: id}, op.o)); // 555 + } else if ((publishedBefore || bufferedBefore) && canDirectlyModifyDoc) { // 556 + // Oh great, we actually know what the document is, so we can apply // 557 + // this directly. // 558 + var newDoc = self._published.has(id) ? // 559 + self._published.get(id) : // 560 + self._unpublishedBuffer.get(id); // 561 + newDoc = EJSON.clone(newDoc); // 562 + // 563 + newDoc._id = id; // 564 + LocalCollection._modify(newDoc, op.o); // 565 + self._handleDoc(id, self._sharedProjectionFn(newDoc)); // 566 + } else if (!canDirectlyModifyDoc || // 567 + self._matcher.canBecomeTrueByModifier(op.o) || // 568 + (self._sorter && self._sorter.affectedByModifier(op.o))) { // 569 + self._needToFetch.set(id, op.ts.toString()); // 570 + if (self._phase === PHASE.STEADY) // 571 + self._fetchModifiedDocuments(); // 572 + } // 573 + } else { // 574 + throw Error("XXX SURPRISING OPERATION: " + op); // 575 + } // 576 + }, // 577 + _runInitialQuery: function () { // 578 + var self = this; // 579 + if (self._stopped) // 580 + throw new Error("oplog stopped surprisingly early"); // 581 + // 582 + self._runQuery(); // 583 + // 584 + if (self._stopped) // 585 + throw new Error("oplog stopped quite early"); // 586 + // Allow observeChanges calls to return. (After this, it's possible for // 587 + // stop() to be called.) // 588 + self._multiplexer.ready(); // 589 + // 590 + self._doneQuerying(); // 591 + }, // 592 + // 593 + // In various circumstances, we may just want to stop processing the oplog and // 594 + // re-run the initial query, just as if we were a PollingObserveDriver. // 595 + // // 596 + // This function may not block, because it is called from an oplog entry // 597 + // handler. // 598 + // // 599 + // XXX We should call this when we detect that we've been in FETCHING for "too // 600 + // long". // 601 + // // 602 + // XXX We should call this when we detect Mongo failover (since that might // 603 + // mean that some of the oplog entries we have processed have been rolled // 604 + // back). The Node Mongo driver is in the middle of a bunch of huge // 605 + // refactorings, including the way that it notifies you when primary // 606 + // changes. Will put off implementing this until driver 1.4 is out. // 607 + _pollQuery: function () { // 608 + var self = this; // 609 + // 610 + if (self._stopped) // 611 + return; // 612 + // 613 + // Yay, we get to forget about all the things we thought we had to fetch. // 614 + self._needToFetch = new LocalCollection._IdMap; // 615 + self._currentlyFetching = null; // 616 + ++self._fetchGeneration; // ignore any in-flight fetches // 617 + self._registerPhaseChange(PHASE.QUERYING); // 618 + // 619 + // Defer so that we don't block. We don't need finishIfNeedToPollQuery here // 620 + // because SwitchedToQuery is not called in QUERYING mode. // 621 + Meteor.defer(function () { // 622 + self._runQuery(); // 623 + self._doneQuerying(); // 624 + }); // 625 + }, // 626 + // 627 + _runQuery: function () { // 628 + var self = this; // 629 + var newResults, newBuffer; // 630 + // 631 + // This while loop is just to retry failures. // 632 + while (true) { // 633 + // If we've been stopped, we don't have to run anything any more. // 634 + if (self._stopped) // 635 + return; // 636 + // 637 + newResults = new LocalCollection._IdMap; // 638 + newBuffer = new LocalCollection._IdMap; // 639 + // 640 + // Query 2x documents as the half excluded from the original query will go // 641 + // into unpublished buffer to reduce additional Mongo lookups in cases // 642 + // when documents are removed from the published set and need a // 643 + // replacement. // 644 + // XXX needs more thought on non-zero skip // 645 + // XXX 2 is a "magic number" meaning there is an extra chunk of docs for // 646 + // buffer if such is needed. // 647 + var cursor = self._cursorForQuery({ limit: self._limit * 2 }); // 648 + try { // 649 + cursor.forEach(function (doc, i) { // 650 + if (!self._limit || i < self._limit) // 651 + newResults.set(doc._id, doc); // 652 + else // 653 + newBuffer.set(doc._id, doc); // 654 + }); // 655 + break; // 656 + } catch (e) { // 657 + // During failover (eg) if we get an exception we should log and retry // 658 + // instead of crashing. // 659 + Meteor._debug("Got exception while polling query: " + e); // 660 + Meteor._sleepForMs(100); // 661 + } // 662 + } // 663 + // 664 + if (self._stopped) // 665 + return; // 666 + // 667 + self._publishNewResults(newResults, newBuffer); // 668 + }, // 669 + // 670 + // Transitions to QUERYING and runs another query, or (if already in QUERYING) // 671 + // ensures that we will query again later. // 672 + // // 673 + // This function may not block, because it is called from an oplog entry // 674 + // handler. However, if we were not already in the QUERYING phase, it throws // 675 + // an exception that is caught by the closest surrounding // 676 + // finishIfNeedToPollQuery call; this ensures that we don't continue running // 677 + // close that was designed for another phase inside PHASE.QUERYING. // 678 + // // 679 + // (It's also necessary whenever logic in this file yields to check that other // 680 + // phases haven't put us into QUERYING mode, though; eg, // 681 + // _fetchModifiedDocuments does this.) // 682 + _needToPollQuery: function () { // 683 + var self = this; // 684 + if (self._stopped) // 685 + return; // 686 + // 687 + // If we're not already in the middle of a query, we can query now (possibly // 688 + // pausing FETCHING). // 689 + if (self._phase !== PHASE.QUERYING) { // 690 + self._pollQuery(); // 691 + throw new SwitchedToQuery; // 692 + } // 693 + // 694 + // We're currently in QUERYING. Set a flag to ensure that we run another // 695 + // query when we're done. // 696 + self._requeryWhenDoneThisQuery = true; // 697 + }, // 698 + // 699 + _doneQuerying: function () { // 700 + var self = this; // 701 + // 702 + if (self._stopped) // 703 + return; // 704 + self._mongoHandle._oplogHandle.waitUntilCaughtUp(); // 705 + // 706 + if (self._stopped) // 707 + return; // 708 + if (self._phase !== PHASE.QUERYING) // 709 + throw Error("Phase unexpectedly " + self._phase); // 710 + // 711 + if (self._requeryWhenDoneThisQuery) { // 712 + self._requeryWhenDoneThisQuery = false; // 713 + self._pollQuery(); // 714 + } else if (self._needToFetch.empty()) { // 715 + self._beSteady(); // 716 + } else { // 717 + self._fetchModifiedDocuments(); // 718 + } // 719 + }, // 720 + // 721 + _cursorForQuery: function (optionsOverwrite) { // 722 + var self = this; // 723 + // 724 + // The query we run is almost the same as the cursor we are observing, with // 725 + // a few changes. We need to read all the fields that are relevant to the // 726 + // selector, not just the fields we are going to publish (that's the // 727 + // "shared" projection). And we don't want to apply any transform in the // 728 + // cursor, because observeChanges shouldn't use the transform. // 729 + var options = _.clone(self._cursorDescription.options); // 730 + // 731 + // Allow the caller to modify the options. Useful to specify different skip // 732 + // and limit values. // 733 + _.extend(options, optionsOverwrite); // 734 + // 735 + options.fields = self._sharedProjection; // 736 + delete options.transform; // 737 + // We are NOT deep cloning fields or selector here, which should be OK. // 738 + var description = new CursorDescription( // 739 + self._cursorDescription.collectionName, // 740 + self._cursorDescription.selector, // 741 + options); // 742 + return new Cursor(self._mongoHandle, description); // 743 + }, // 744 + // 745 + // 746 + // Replace self._published with newResults (both are IdMaps), invoking observe // 747 + // callbacks on the multiplexer. // 748 + // Replace self._unpublishedBuffer with newBuffer. // 749 + // // 750 + // XXX This is very similar to LocalCollection._diffQueryUnorderedChanges. We // 751 + // should really: (a) Unify IdMap and OrderedDict into Unordered/OrderedDict (b) // 752 + // Rewrite diff.js to use these classes instead of arrays and objects. // 753 + _publishNewResults: function (newResults, newBuffer) { // 754 + var self = this; // 755 + // 756 + // If the query is limited and there is a buffer, shut down so it doesn't // 757 + // stay in a way. // 758 + if (self._limit) { // 759 + self._unpublishedBuffer.clear(); // 760 + } // 761 + // 762 + // First remove anything that's gone. Be careful not to modify // 763 + // self._published while iterating over it. // 764 + var idsToRemove = []; // 765 + self._published.forEach(function (doc, id) { // 766 + if (!newResults.has(id)) // 767 + idsToRemove.push(id); // 768 + }); // 769 + _.each(idsToRemove, function (id) { // 770 + self._removePublished(id); // 771 + }); // 772 + // 773 + // Now do adds and changes. // 774 + // If self has a buffer and limit, the new fetched result will be // 775 + // limited correctly as the query has sort specifier. // 776 + newResults.forEach(function (doc, id) { // 777 + self._handleDoc(id, doc); // 778 + }); // 779 + // 780 + // Sanity-check that everything we tried to put into _published ended up // 781 + // there. // 782 + // XXX if this is slow, remove it later // 783 + if (self._published.size() !== newResults.size()) { // 784 + throw Error("failed to copy newResults into _published!"); // 785 + } // 786 + self._published.forEach(function (doc, id) { // 787 + if (!newResults.has(id)) // 788 + throw Error("_published has a doc that newResults doesn't; " + id); // 789 + }); // 790 + // 791 + // Finally, replace the buffer // 792 + newBuffer.forEach(function (doc, id) { // 793 + self._addBuffered(id, doc); // 794 + }); // 795 + // 796 + self._safeAppendToBuffer = newBuffer.size() < self._limit; // 797 + }, // 798 + // 799 + // This stop function is invoked from the onStop of the ObserveMultiplexer, so // 800 + // it shouldn't actually be possible to call it until the multiplexer is // 801 + // ready. // 802 + // // 803 + // It's important to check self._stopped after every call in this file that // 804 + // can yield! // 805 + stop: function () { // 806 + var self = this; // 807 + if (self._stopped) // 808 + return; // 809 + self._stopped = true; // 810 + _.each(self._stopHandles, function (handle) { // 811 + handle.stop(); // 812 + }); // 813 + // 814 + // Note: we *don't* use multiplexer.onFlush here because this stop // 815 + // callback is actually invoked by the multiplexer itself when it has // 816 + // determined that there are no handles left. So nothing is actually going // 817 + // to get flushed (and it's probably not valid to call methods on the // 818 + // dying multiplexer). // 819 + _.each(self._writesToCommitWhenWeReachSteady, function (w) { // 820 + w.committed(); // 821 + }); // 822 + self._writesToCommitWhenWeReachSteady = null; // 823 + // 824 + // Proactively drop references to potentially big things. // 825 + self._published = null; // 826 + self._unpublishedBuffer = null; // 827 + self._needToFetch = null; // 828 + self._currentlyFetching = null; // 829 + self._oplogEntryHandle = null; // 830 + self._listenersHandle = null; // 831 + // 832 + Package.facts && Package.facts.Facts.incrementServerFact( // 833 + "mongo-livedata", "observe-drivers-oplog", -1); // 834 + }, // 835 + // 836 + _registerPhaseChange: function (phase) { // 837 + var self = this; // 838 + var now = new Date; // 839 + // 840 + if (self._phase) { // 841 + var timeDiff = now - self._phaseStartTime; // 842 + Package.facts && Package.facts.Facts.incrementServerFact( // 843 + "mongo-livedata", "time-spent-in-" + self._phase + "-phase", timeDiff); // 844 + } // 845 + // 846 + self._phase = phase; // 847 + self._phaseStartTime = now; // 848 + } // 849 +}); // 850 + // 851 +// Does our oplog tailing code support this cursor? For now, we are being very // 852 +// conservative and allowing only simple queries with simple options. // 853 +// (This is a "static method".) // 854 +OplogObserveDriver.cursorSupported = function (cursorDescription, matcher) { // 855 + // First, check the options. // 856 + var options = cursorDescription.options; // 857 + // 858 + // Did the user say no explicitly? // 859 + if (options._disableOplog) // 860 + return false; // 861 + // 862 + // skip is not supported: to support it we would need to keep track of all // 863 + // "skipped" documents or at least their ids. // 864 + // limit w/o a sort specifier is not supported: current implementation needs a // 865 + // deterministic way to order documents. // 866 + if (options.skip || (options.limit && !options.sort)) return false; // 867 + // 868 + // If a fields projection option is given check if it is supported by // 869 + // minimongo (some operators are not supported). // 870 + if (options.fields) { // 871 + try { // 872 + LocalCollection._checkSupportedProjection(options.fields); // 873 + } catch (e) { // 874 + if (e.name === "MinimongoError") // 875 + return false; // 876 + else // 877 + throw e; // 878 + } // 879 + } // 880 + // 881 + // We don't allow the following selectors: // 882 + // - $where (not confident that we provide the same JS environment // 883 + // as Mongo, and can yield!) // 884 + // - $near (has "interesting" properties in MongoDB, like the possibility // 885 + // of returning an ID multiple times, though even polling maybe // 886 + // have a bug there) // 887 + // XXX: once we support it, we would need to think more on how we // 888 + // initialize the comparators when we create the driver. // 889 + return !matcher.hasWhere() && !matcher.hasGeoQuery(); // 890 +}; // 891 + // 892 +var modifierCanBeDirectlyApplied = function (modifier) { // 893 + return _.all(modifier, function (fields, operation) { // 894 + return _.all(fields, function (value, field) { // 895 + return !/EJSON\$/.test(field); // 896 + }); // 897 + }); // 898 +}; // 899 + // 900 +MongoInternals.OplogObserveDriver = OplogObserveDriver; // 901 + // 902 +///////////////////////////////////////////////////////////////////////////////////////////////////////// + +}).call(this); + + + + + + +(function () { + +///////////////////////////////////////////////////////////////////////////////////////////////////////// +// // +// packages/mongo-livedata/local_collection_driver.js // +// // +///////////////////////////////////////////////////////////////////////////////////////////////////////// + // +LocalCollectionDriver = function () { // 1 + var self = this; // 2 + self.noConnCollections = {}; // 3 +}; // 4 + // 5 +var ensureCollection = function (name, collections) { // 6 + if (!(name in collections)) // 7 + collections[name] = new LocalCollection(name); // 8 + return collections[name]; // 9 +}; // 10 + // 11 +_.extend(LocalCollectionDriver.prototype, { // 12 + open: function (name, conn) { // 13 + var self = this; // 14 + if (!name) // 15 + return new LocalCollection; // 16 + if (! conn) { // 17 + return ensureCollection(name, self.noConnCollections); // 18 + } // 19 + if (! conn._mongo_livedata_collections) // 20 + conn._mongo_livedata_collections = {}; // 21 + // XXX is there a way to keep track of a connection's collections without // 22 + // dangling it off the connection object? // 23 + return ensureCollection(name, conn._mongo_livedata_collections); // 24 + } // 25 +}); // 26 + // 27 +// singleton // 28 +LocalCollectionDriver = new LocalCollectionDriver; // 29 + // 30 +///////////////////////////////////////////////////////////////////////////////////////////////////////// + +}).call(this); + + + + + + +(function () { + +///////////////////////////////////////////////////////////////////////////////////////////////////////// +// // +// packages/mongo-livedata/remote_collection_driver.js // +// // +///////////////////////////////////////////////////////////////////////////////////////////////////////// + // +MongoInternals.RemoteCollectionDriver = function ( // 1 + mongo_url, options) { // 2 + var self = this; // 3 + self.mongo = new MongoConnection(mongo_url, options); // 4 +}; // 5 + // 6 +_.extend(MongoInternals.RemoteCollectionDriver.prototype, { // 7 + open: function (name) { // 8 + var self = this; // 9 + var ret = {}; // 10 + _.each( // 11 + ['find', 'findOne', 'insert', 'update', , 'upsert', // 12 + 'remove', '_ensureIndex', '_dropIndex', '_createCappedCollection', // 13 + 'dropCollection'], // 14 + function (m) { // 15 + ret[m] = _.bind(self.mongo[m], self.mongo, name); // 16 + }); // 17 + return ret; // 18 + } // 19 +}); // 20 + // 21 + // 22 +// Create the singleton RemoteCollectionDriver only on demand, so we // 23 +// only require Mongo configuration if it's actually used (eg, not if // 24 +// you're only trying to receive data from a remote DDP server.) // 25 +MongoInternals.defaultRemoteCollectionDriver = _.once(function () { // 26 + var mongoUrl; // 27 + var connectionOptions = {}; // 28 + // 29 + AppConfig.configurePackage("mongo-livedata", function (config) { // 30 + // This will keep running if mongo gets reconfigured. That's not ideal, but // 31 + // should be ok for now. // 32 + mongoUrl = config.url; // 33 + // 34 + if (config.oplog) // 35 + connectionOptions.oplogUrl = config.oplog; // 36 + }); // 37 + // 38 + // XXX bad error since it could also be set directly in METEOR_DEPLOY_CONFIG // 39 + if (! mongoUrl) // 40 + throw new Error("MONGO_URL must be set in environment"); // 41 + // 42 + // 43 + return new MongoInternals.RemoteCollectionDriver(mongoUrl, connectionOptions); // 44 +}); // 45 + // 46 +///////////////////////////////////////////////////////////////////////////////////////////////////////// + +}).call(this); + + + + + + +(function () { + +///////////////////////////////////////////////////////////////////////////////////////////////////////// +// // +// packages/mongo-livedata/collection.js // +// // +///////////////////////////////////////////////////////////////////////////////////////////////////////// + // +// options.connection, if given, is a LivedataClient or LivedataServer // 1 +// XXX presently there is no way to destroy/clean up a Collection // 2 + // 3 +Meteor.Collection = function (name, options) { // 4 + var self = this; // 5 + if (! (self instanceof Meteor.Collection)) // 6 + throw new Error('use "new" to construct a Meteor.Collection'); // 7 + // 8 + if (!name && (name !== null)) { // 9 + Meteor._debug("Warning: creating anonymous collection. It will not be " + // 10 + "saved or synchronized over the network. (Pass null for " + // 11 + "the collection name to turn off this warning.)"); // 12 + name = null; // 13 + } // 14 + // 15 + if (name !== null && typeof name !== "string") { // 16 + throw new Error( // 17 + "First argument to new Meteor.Collection must be a string or null"); // 18 + } // 19 + // 20 + if (options && options.methods) { // 21 + // Backwards compatibility hack with original signature (which passed // 22 + // "connection" directly instead of in options. (Connections must have a "methods" // 23 + // method.) // 24 + // XXX remove before 1.0 // 25 + options = {connection: options}; // 26 + } // 27 + // Backwards compatibility: "connection" used to be called "manager". // 28 + if (options && options.manager && !options.connection) { // 29 + options.connection = options.manager; // 30 + } // 31 + options = _.extend({ // 32 + connection: undefined, // 33 + idGeneration: 'STRING', // 34 + transform: null, // 35 + _driver: undefined, // 36 + _preventAutopublish: false // 37 + }, options); // 38 + // 39 + switch (options.idGeneration) { // 40 + case 'MONGO': // 41 + self._makeNewID = function () { // 42 + var src = name ? DDP.randomStream('/collection/' + name) : Random; // 43 + return new Meteor.Collection.ObjectID(src.hexString(24)); // 44 + }; // 45 + break; // 46 + case 'STRING': // 47 + default: // 48 + self._makeNewID = function () { // 49 + var src = name ? DDP.randomStream('/collection/' + name) : Random; // 50 + return src.id(); // 51 + }; // 52 + break; // 53 + } // 54 + // 55 + self._transform = LocalCollection.wrapTransform(options.transform); // 56 + // 57 + if (! name || options.connection === null) // 58 + // note: nameless collections never have a connection // 59 + self._connection = null; // 60 + else if (options.connection) // 61 + self._connection = options.connection; // 62 + else if (Meteor.isClient) // 63 + self._connection = Meteor.connection; // 64 + else // 65 + self._connection = Meteor.server; // 66 + // 67 + if (!options._driver) { // 68 + if (name && self._connection === Meteor.server && // 69 + typeof MongoInternals !== "undefined" && // 70 + MongoInternals.defaultRemoteCollectionDriver) { // 71 + options._driver = MongoInternals.defaultRemoteCollectionDriver(); // 72 + } else { // 73 + options._driver = LocalCollectionDriver; // 74 + } // 75 + } // 76 + // 77 + self._collection = options._driver.open(name, self._connection); // 78 + self._name = name; // 79 + // 80 + if (self._connection && self._connection.registerStore) { // 81 + // OK, we're going to be a slave, replicating some remote // 82 + // database, except possibly with some temporary divergence while // 83 + // we have unacknowledged RPC's. // 84 + var ok = self._connection.registerStore(name, { // 85 + // Called at the beginning of a batch of updates. batchSize is the number // 86 + // of update calls to expect. // 87 + // // 88 + // XXX This interface is pretty janky. reset probably ought to go back to // 89 + // being its own function, and callers shouldn't have to calculate // 90 + // batchSize. The optimization of not calling pause/remove should be // 91 + // delayed until later: the first call to update() should buffer its // 92 + // message, and then we can either directly apply it at endUpdate time if // 93 + // it was the only update, or do pauseObservers/apply/apply at the next // 94 + // update() if there's another one. // 95 + beginUpdate: function (batchSize, reset) { // 96 + // pause observers so users don't see flicker when updating several // 97 + // objects at once (including the post-reconnect reset-and-reapply // 98 + // stage), and so that a re-sorting of a query can take advantage of the // 99 + // full _diffQuery moved calculation instead of applying change one at a // 100 + // time. // 101 + if (batchSize > 1 || reset) // 102 + self._collection.pauseObservers(); // 103 + // 104 + if (reset) // 105 + self._collection.remove({}); // 106 + }, // 107 + // 108 + // Apply an update. // 109 + // XXX better specify this interface (not in terms of a wire message)? // 110 + update: function (msg) { // 111 + var mongoId = LocalCollection._idParse(msg.id); // 112 + var doc = self._collection.findOne(mongoId); // 113 + // 114 + // Is this a "replace the whole doc" message coming from the quiescence // 115 + // of method writes to an object? (Note that 'undefined' is a valid // 116 + // value meaning "remove it".) // 117 + if (msg.msg === 'replace') { // 118 + var replace = msg.replace; // 119 + if (!replace) { // 120 + if (doc) // 121 + self._collection.remove(mongoId); // 122 + } else if (!doc) { // 123 + self._collection.insert(replace); // 124 + } else { // 125 + // XXX check that replace has no $ ops // 126 + self._collection.update(mongoId, replace); // 127 + } // 128 + return; // 129 + } else if (msg.msg === 'added') { // 130 + if (doc) { // 131 + throw new Error("Expected not to find a document already present for an add"); // 132 + } // 133 + self._collection.insert(_.extend({_id: mongoId}, msg.fields)); // 134 + } else if (msg.msg === 'removed') { // 135 + if (!doc) // 136 + throw new Error("Expected to find a document already present for removed"); // 137 + self._collection.remove(mongoId); // 138 + } else if (msg.msg === 'changed') { // 139 + if (!doc) // 140 + throw new Error("Expected to find a document to change"); // 141 + if (!_.isEmpty(msg.fields)) { // 142 + var modifier = {}; // 143 + _.each(msg.fields, function (value, key) { // 144 + if (value === undefined) { // 145 + if (!modifier.$unset) // 146 + modifier.$unset = {}; // 147 + modifier.$unset[key] = 1; // 148 + } else { // 149 + if (!modifier.$set) // 150 + modifier.$set = {}; // 151 + modifier.$set[key] = value; // 152 + } // 153 + }); // 154 + self._collection.update(mongoId, modifier); // 155 + } // 156 + } else { // 157 + throw new Error("I don't know how to deal with this message"); // 158 + } // 159 + // 160 + }, // 161 + // 162 + // Called at the end of a batch of updates. // 163 + endUpdate: function () { // 164 + self._collection.resumeObservers(); // 165 + }, // 166 + // 167 + // Called around method stub invocations to capture the original versions // 168 + // of modified documents. // 169 + saveOriginals: function () { // 170 + self._collection.saveOriginals(); // 171 + }, // 172 + retrieveOriginals: function () { // 173 + return self._collection.retrieveOriginals(); // 174 + } // 175 + }); // 176 + // 177 + if (!ok) // 178 + throw new Error("There is already a collection named '" + name + "'"); // 179 + } // 180 + // 181 + self._defineMutationMethods(); // 182 + // 183 + // autopublish // 184 + if (Package.autopublish && !options._preventAutopublish && self._connection // 185 + && self._connection.publish) { // 186 + self._connection.publish(null, function () { // 187 + return self.find(); // 188 + }, {is_auto: true}); // 189 + } // 190 +}; // 191 + // 192 +/// // 193 +/// Main collection API // 194 +/// // 195 + // 196 + // 197 +_.extend(Meteor.Collection.prototype, { // 198 + // 199 + _getFindSelector: function (args) { // 200 + if (args.length == 0) // 201 + return {}; // 202 + else // 203 + return args[0]; // 204 + }, // 205 + // 206 + _getFindOptions: function (args) { // 207 + var self = this; // 208 + if (args.length < 2) { // 209 + return { transform: self._transform }; // 210 + } else { // 211 + check(args[1], Match.Optional(Match.ObjectIncluding({ // 212 + fields: Match.Optional(Match.OneOf(Object, undefined)), // 213 + sort: Match.Optional(Match.OneOf(Object, Array, undefined)), // 214 + limit: Match.Optional(Match.OneOf(Number, undefined)), // 215 + skip: Match.Optional(Match.OneOf(Number, undefined)) // 216 + }))); // 217 + // 218 + return _.extend({ // 219 + transform: self._transform // 220 + }, args[1]); // 221 + } // 222 + }, // 223 + // 224 + find: function (/* selector, options */) { // 225 + // Collection.find() (return all docs) behaves differently // 226 + // from Collection.find(undefined) (return 0 docs). so be // 227 + // careful about the length of arguments. // 228 + var self = this; // 229 + var argArray = _.toArray(arguments); // 230 + return self._collection.find(self._getFindSelector(argArray), // 231 + self._getFindOptions(argArray)); // 232 + }, // 233 + // 234 + findOne: function (/* selector, options */) { // 235 + var self = this; // 236 + var argArray = _.toArray(arguments); // 237 + return self._collection.findOne(self._getFindSelector(argArray), // 238 + self._getFindOptions(argArray)); // 239 + } // 240 + // 241 +}); // 242 + // 243 +Meteor.Collection._publishCursor = function (cursor, sub, collection) { // 244 + var observeHandle = cursor.observeChanges({ // 245 + added: function (id, fields) { // 246 + sub.added(collection, id, fields); // 247 + }, // 248 + changed: function (id, fields) { // 249 + sub.changed(collection, id, fields); // 250 + }, // 251 + removed: function (id) { // 252 + sub.removed(collection, id); // 253 + } // 254 + }); // 255 + // 256 + // We don't call sub.ready() here: it gets called in livedata_server, after // 257 + // possibly calling _publishCursor on multiple returned cursors. // 258 + // 259 + // register stop callback (expects lambda w/ no args). // 260 + sub.onStop(function () {observeHandle.stop();}); // 261 +}; // 262 + // 263 +// protect against dangerous selectors. falsey and {_id: falsey} are both // 264 +// likely programmer error, and not what you want, particularly for destructive // 265 +// operations. JS regexps don't serialize over DDP but can be trivially // 266 +// replaced by $regex. // 267 +Meteor.Collection._rewriteSelector = function (selector) { // 268 + // shorthand -- scalars match _id // 269 + if (LocalCollection._selectorIsId(selector)) // 270 + selector = {_id: selector}; // 271 + // 272 + if (!selector || (('_id' in selector) && !selector._id)) // 273 + // can't match anything // 274 + return {_id: Random.id()}; // 275 + // 276 + var ret = {}; // 277 + _.each(selector, function (value, key) { // 278 + // Mongo supports both {field: /foo/} and {field: {$regex: /foo/}} // 279 + if (value instanceof RegExp) { // 280 + ret[key] = convertRegexpToMongoSelector(value); // 281 + } else if (value && value.$regex instanceof RegExp) { // 282 + ret[key] = convertRegexpToMongoSelector(value.$regex); // 283 + // if value is {$regex: /foo/, $options: ...} then $options // 284 + // override the ones set on $regex. // 285 + if (value.$options !== undefined) // 286 + ret[key].$options = value.$options; // 287 + } // 288 + else if (_.contains(['$or','$and','$nor'], key)) { // 289 + // Translate lower levels of $and/$or/$nor // 290 + ret[key] = _.map(value, function (v) { // 291 + return Meteor.Collection._rewriteSelector(v); // 292 + }); // 293 + } else { // 294 + ret[key] = value; // 295 + } // 296 + }); // 297 + return ret; // 298 +}; // 299 + // 300 +// convert a JS RegExp object to a Mongo {$regex: ..., $options: ...} // 301 +// selector // 302 +var convertRegexpToMongoSelector = function (regexp) { // 303 + check(regexp, RegExp); // safety belt // 304 + // 305 + var selector = {$regex: regexp.source}; // 306 + var regexOptions = ''; // 307 + // JS RegExp objects support 'i', 'm', and 'g'. Mongo regex $options // 308 + // support 'i', 'm', 'x', and 's'. So we support 'i' and 'm' here. // 309 + if (regexp.ignoreCase) // 310 + regexOptions += 'i'; // 311 + if (regexp.multiline) // 312 + regexOptions += 'm'; // 313 + if (regexOptions) // 314 + selector.$options = regexOptions; // 315 + // 316 + return selector; // 317 +}; // 318 + // 319 +var throwIfSelectorIsNotId = function (selector, methodName) { // 320 + if (!LocalCollection._selectorIsIdPerhapsAsObject(selector)) { // 321 + throw new Meteor.Error( // 322 + 403, "Not permitted. Untrusted code may only " + methodName + // 323 + " documents by ID."); // 324 + } // 325 +}; // 326 + // 327 +// 'insert' immediately returns the inserted document's new _id. // 328 +// The others return values immediately if you are in a stub, an in-memory // 329 +// unmanaged collection, or a mongo-backed collection and you don't pass a // 330 +// callback. 'update' and 'remove' return the number of affected // 331 +// documents. 'upsert' returns an object with keys 'numberAffected' and, if an // 332 +// insert happened, 'insertedId'. // 333 +// // 334 +// Otherwise, the semantics are exactly like other methods: they take // 335 +// a callback as an optional last argument; if no callback is // 336 +// provided, they block until the operation is complete, and throw an // 337 +// exception if it fails; if a callback is provided, then they don't // 338 +// necessarily block, and they call the callback when they finish with error and // 339 +// result arguments. (The insert method provides the document ID as its result; // 340 +// update and remove provide the number of affected docs as the result; upsert // 341 +// provides an object with numberAffected and maybe insertedId.) // 342 +// // 343 +// On the client, blocking is impossible, so if a callback // 344 +// isn't provided, they just return immediately and any error // 345 +// information is lost. // 346 +// // 347 +// There's one more tweak. On the client, if you don't provide a // 348 +// callback, then if there is an error, a message will be logged with // 349 +// Meteor._debug. // 350 +// // 351 +// The intent (though this is actually determined by the underlying // 352 +// drivers) is that the operations should be done synchronously, not // 353 +// generating their result until the database has acknowledged // 354 +// them. In the future maybe we should provide a flag to turn this // 355 +// off. // 356 +_.each(["insert", "update", "remove"], function (name) { // 357 + Meteor.Collection.prototype[name] = function (/* arguments */) { // 358 + var self = this; // 359 + var args = _.toArray(arguments); // 360 + var callback; // 361 + var insertId; // 362 + var ret; // 363 + // 364 + if (args.length && args[args.length - 1] instanceof Function) // 365 + callback = args.pop(); // 366 + // 367 + if (name === "insert") { // 368 + if (!args.length) // 369 + throw new Error("insert requires an argument"); // 370 + // shallow-copy the document and generate an ID // 371 + args[0] = _.extend({}, args[0]); // 372 + if ('_id' in args[0]) { // 373 + insertId = args[0]._id; // 374 + if (!insertId || !(typeof insertId === 'string' // 375 + || insertId instanceof Meteor.Collection.ObjectID)) // 376 + throw new Error("Meteor requires document _id fields to be non-empty strings or ObjectIDs"); // 377 + } else { // 378 + var generateId = true; // 379 + // Don't generate the id if we're the client and the 'outermost' call // 380 + // This optimization saves us passing both the randomSeed and the id // 381 + // Passing both is redundant. // 382 + if (self._connection && self._connection !== Meteor.server) { // 383 + var enclosing = DDP._CurrentInvocation.get(); // 384 + if (!enclosing) { // 385 + generateId = false; // 386 + } // 387 + } // 388 + if (generateId) { // 389 + insertId = args[0]._id = self._makeNewID(); // 390 + } // 391 + } // 392 + } else { // 393 + args[0] = Meteor.Collection._rewriteSelector(args[0]); // 394 + // 395 + if (name === "update") { // 396 + // Mutate args but copy the original options object. We need to add // 397 + // insertedId to options, but don't want to mutate the caller's options // 398 + // object. We need to mutate `args` because we pass `args` into the // 399 + // driver below. // 400 + var options = args[2] = _.clone(args[2]) || {}; // 401 + if (options && typeof options !== "function" && options.upsert) { // 402 + // set `insertedId` if absent. `insertedId` is a Meteor extension. // 403 + if (options.insertedId) { // 404 + if (!(typeof options.insertedId === 'string' // 405 + || options.insertedId instanceof Meteor.Collection.ObjectID)) // 406 + throw new Error("insertedId must be string or ObjectID"); // 407 + } else { // 408 + options.insertedId = self._makeNewID(); // 409 + } // 410 + } // 411 + } // 412 + } // 413 + // 414 + // On inserts, always return the id that we generated; on all other // 415 + // operations, just return the result from the collection. // 416 + var chooseReturnValueFromCollectionResult = function (result) { // 417 + if (name === "insert") { // 418 + if (!insertId && result) { // 419 + insertId = result; // 420 + } // 421 + return insertId; // 422 + } else { // 423 + return result; // 424 + } // 425 + }; // 426 + // 427 + var wrappedCallback; // 428 + if (callback) { // 429 + wrappedCallback = function (error, result) { // 430 + callback(error, ! error && chooseReturnValueFromCollectionResult(result)); // 431 + }; // 432 + } // 433 + // 434 + if (self._connection && self._connection !== Meteor.server) { // 435 + // just remote to another endpoint, propagate return value or // 436 + // exception. // 437 + // 438 + var enclosing = DDP._CurrentInvocation.get(); // 439 + var alreadyInSimulation = enclosing && enclosing.isSimulation; // 440 + // 441 + if (Meteor.isClient && !wrappedCallback && ! alreadyInSimulation) { // 442 + // Client can't block, so it can't report errors by exception, // 443 + // only by callback. If they forget the callback, give them a // 444 + // default one that logs the error, so they aren't totally // 445 + // baffled if their writes don't work because their database is // 446 + // down. // 447 + // Don't give a default callback in simulation, because inside stubs we // 448 + // want to return the results from the local collection immediately and // 449 + // not force a callback. // 450 + wrappedCallback = function (err) { // 451 + if (err) // 452 + Meteor._debug(name + " failed: " + (err.reason || err.stack)); // 453 + }; // 454 + } // 455 + // 456 + if (!alreadyInSimulation && name !== "insert") { // 457 + // If we're about to actually send an RPC, we should throw an error if // 458 + // this is a non-ID selector, because the mutation methods only allow // 459 + // single-ID selectors. (If we don't throw here, we'll see flicker.) // 460 + throwIfSelectorIsNotId(args[0], name); // 461 + } // 462 + // 463 + ret = chooseReturnValueFromCollectionResult( // 464 + self._connection.apply(self._prefix + name, args, {returnStubValue: true}, wrappedCallback) // 465 + ); // 466 + // 467 + } else { // 468 + // it's my collection. descend into the collection object // 469 + // and propagate any exception. // 470 + args.push(wrappedCallback); // 471 + try { // 472 + // If the user provided a callback and the collection implements this // 473 + // operation asynchronously, then queryRet will be undefined, and the // 474 + // result will be returned through the callback instead. // 475 + var queryRet = self._collection[name].apply(self._collection, args); // 476 + ret = chooseReturnValueFromCollectionResult(queryRet); // 477 + } catch (e) { // 478 + if (callback) { // 479 + callback(e); // 480 + return null; // 481 + } // 482 + throw e; // 483 + } // 484 + } // 485 + // 486 + // both sync and async, unless we threw an exception, return ret // 487 + // (new document ID for insert, num affected for update/remove, object with // 488 + // numberAffected and maybe insertedId for upsert). // 489 + return ret; // 490 + }; // 491 +}); // 492 + // 493 +Meteor.Collection.prototype.upsert = function (selector, modifier, // 494 + options, callback) { // 495 + var self = this; // 496 + if (! callback && typeof options === "function") { // 497 + callback = options; // 498 + options = {}; // 499 + } // 500 + return self.update(selector, modifier, // 501 + _.extend({}, options, { _returnObject: true, upsert: true }), // 502 + callback); // 503 +}; // 504 + // 505 +// We'll actually design an index API later. For now, we just pass through to // 506 +// Mongo's, but make it synchronous. // 507 +Meteor.Collection.prototype._ensureIndex = function (index, options) { // 508 + var self = this; // 509 + if (!self._collection._ensureIndex) // 510 + throw new Error("Can only call _ensureIndex on server collections"); // 511 + self._collection._ensureIndex(index, options); // 512 +}; // 513 +Meteor.Collection.prototype._dropIndex = function (index) { // 514 + var self = this; // 515 + if (!self._collection._dropIndex) // 516 + throw new Error("Can only call _dropIndex on server collections"); // 517 + self._collection._dropIndex(index); // 518 +}; // 519 +Meteor.Collection.prototype._dropCollection = function () { // 520 + var self = this; // 521 + if (!self._collection.dropCollection) // 522 + throw new Error("Can only call _dropCollection on server collections"); // 523 + self._collection.dropCollection(); // 524 +}; // 525 +Meteor.Collection.prototype._createCappedCollection = function (byteSize) { // 526 + var self = this; // 527 + if (!self._collection._createCappedCollection) // 528 + throw new Error("Can only call _createCappedCollection on server collections"); // 529 + self._collection._createCappedCollection(byteSize); // 530 +}; // 531 + // 532 +Meteor.Collection.ObjectID = LocalCollection._ObjectID; // 533 + // 534 +/// // 535 +/// Remote methods and access control. // 536 +/// // 537 + // 538 +// Restrict default mutators on collection. allow() and deny() take the // 539 +// same options: // 540 +// // 541 +// options.insert {Function(userId, doc)} // 542 +// return true to allow/deny adding this document // 543 +// // 544 +// options.update {Function(userId, docs, fields, modifier)} // 545 +// return true to allow/deny updating these documents. // 546 +// `fields` is passed as an array of fields that are to be modified // 547 +// // 548 +// options.remove {Function(userId, docs)} // 549 +// return true to allow/deny removing these documents // 550 +// // 551 +// options.fetch {Array} // 552 +// Fields to fetch for these validators. If any call to allow or deny // 553 +// does not have this option then all fields are loaded. // 554 +// // 555 +// allow and deny can be called multiple times. The validators are // 556 +// evaluated as follows: // 557 +// - If neither deny() nor allow() has been called on the collection, // 558 +// then the request is allowed if and only if the "insecure" smart // 559 +// package is in use. // 560 +// - Otherwise, if any deny() function returns true, the request is denied. // 561 +// - Otherwise, if any allow() function returns true, the request is allowed. // 562 +// - Otherwise, the request is denied. // 563 +// // 564 +// Meteor may call your deny() and allow() functions in any order, and may not // 565 +// call all of them if it is able to make a decision without calling them all // 566 +// (so don't include side effects). // 567 + // 568 +(function () { // 569 + var addValidator = function(allowOrDeny, options) { // 570 + // validate keys // 571 + var VALID_KEYS = ['insert', 'update', 'remove', 'fetch', 'transform']; // 572 + _.each(_.keys(options), function (key) { // 573 + if (!_.contains(VALID_KEYS, key)) // 574 + throw new Error(allowOrDeny + ": Invalid key: " + key); // 575 + }); // 576 + // 577 + var self = this; // 578 + self._restricted = true; // 579 + // 580 + _.each(['insert', 'update', 'remove'], function (name) { // 581 + if (options[name]) { // 582 + if (!(options[name] instanceof Function)) { // 583 + throw new Error(allowOrDeny + ": Value for `" + name + "` must be a function"); // 584 + } // 585 + // 586 + // If the transform is specified at all (including as 'null') in this // 587 + // call, then take that; otherwise, take the transform from the // 588 + // collection. // 589 + if (options.transform === undefined) { // 590 + options[name].transform = self._transform; // already wrapped // 591 + } else { // 592 + options[name].transform = LocalCollection.wrapTransform( // 593 + options.transform); // 594 + } // 595 + // 596 + self._validators[name][allowOrDeny].push(options[name]); // 597 + } // 598 + }); // 599 + // 600 + // Only update the fetch fields if we're passed things that affect // 601 + // fetching. This way allow({}) and allow({insert: f}) don't result in // 602 + // setting fetchAllFields // 603 + if (options.update || options.remove || options.fetch) { // 604 + if (options.fetch && !(options.fetch instanceof Array)) { // 605 + throw new Error(allowOrDeny + ": Value for `fetch` must be an array"); // 606 + } // 607 + self._updateFetch(options.fetch); // 608 + } // 609 + }; // 610 + // 611 + Meteor.Collection.prototype.allow = function(options) { // 612 + addValidator.call(this, 'allow', options); // 613 + }; // 614 + Meteor.Collection.prototype.deny = function(options) { // 615 + addValidator.call(this, 'deny', options); // 616 + }; // 617 +})(); // 618 + // 619 + // 620 +Meteor.Collection.prototype._defineMutationMethods = function() { // 621 + var self = this; // 622 + // 623 + // set to true once we call any allow or deny methods. If true, use // 624 + // allow/deny semantics. If false, use insecure mode semantics. // 625 + self._restricted = false; // 626 + // 627 + // Insecure mode (default to allowing writes). Defaults to 'undefined' which // 628 + // means insecure iff the insecure package is loaded. This property can be // 629 + // overriden by tests or packages wishing to change insecure mode behavior of // 630 + // their collections. // 631 + self._insecure = undefined; // 632 + // 633 + self._validators = { // 634 + insert: {allow: [], deny: []}, // 635 + update: {allow: [], deny: []}, // 636 + remove: {allow: [], deny: []}, // 637 + upsert: {allow: [], deny: []}, // dummy arrays; can't set these! // 638 + fetch: [], // 639 + fetchAllFields: false // 640 + }; // 641 + // 642 + if (!self._name) // 643 + return; // anonymous collection // 644 + // 645 + // XXX Think about method namespacing. Maybe methods should be // 646 + // "Meteor:Mongo:insert/NAME"? // 647 + self._prefix = '/' + self._name + '/'; // 648 + // 649 + // mutation methods // 650 + if (self._connection) { // 651 + var m = {}; // 652 + // 653 + _.each(['insert', 'update', 'remove'], function (method) { // 654 + m[self._prefix + method] = function (/* ... */) { // 655 + // All the methods do their own validation, instead of using check(). // 656 + check(arguments, [Match.Any]); // 657 + var args = _.toArray(arguments); // 658 + try { // 659 + // For an insert, if the client didn't specify an _id, generate one // 660 + // now; because this uses DDP.randomStream, it will be consistent with // 661 + // what the client generated. We generate it now rather than later so // 662 + // that if (eg) an allow/deny rule does an insert to the same // 663 + // collection (not that it really should), the generated _id will // 664 + // still be the first use of the stream and will be consistent. // 665 + // // 666 + // However, we don't actually stick the _id onto the document yet, // 667 + // because we want allow/deny rules to be able to differentiate // 668 + // between arbitrary client-specified _id fields and merely // 669 + // client-controlled-via-randomSeed fields. // 670 + var generatedId = null; // 671 + if (method === "insert" && !_.has(args[0], '_id')) { // 672 + generatedId = self._makeNewID(); // 673 + } // 674 + // 675 + if (this.isSimulation) { // 676 + // In a client simulation, you can do any mutation (even with a // 677 + // complex selector). // 678 + if (generatedId !== null) // 679 + args[0]._id = generatedId; // 680 + return self._collection[method].apply( // 681 + self._collection, args); // 682 + } // 683 + // 684 + // This is the server receiving a method call from the client. // 685 + // 686 + // We don't allow arbitrary selectors in mutations from the client: only // 687 + // single-ID selectors. // 688 + if (method !== 'insert') // 689 + throwIfSelectorIsNotId(args[0], method); // 690 + // 691 + if (self._restricted) { // 692 + // short circuit if there is no way it will pass. // 693 + if (self._validators[method].allow.length === 0) { // 694 + throw new Meteor.Error( // 695 + 403, "Access denied. No allow validators set on restricted " + // 696 + "collection for method '" + method + "'."); // 697 + } // 698 + // 699 + var validatedMethodName = // 700 + '_validated' + method.charAt(0).toUpperCase() + method.slice(1); // 701 + args.unshift(this.userId); // 702 + method === 'insert' && args.push(generatedId); // 703 + return self[validatedMethodName].apply(self, args); // 704 + } else if (self._isInsecure()) { // 705 + if (generatedId !== null) // 706 + args[0]._id = generatedId; // 707 + // In insecure mode, allow any mutation (with a simple selector). // 708 + return self._collection[method].apply(self._collection, args); // 709 + } else { // 710 + // In secure mode, if we haven't called allow or deny, then nothing // 711 + // is permitted. // 712 + throw new Meteor.Error(403, "Access denied"); // 713 + } // 714 + } catch (e) { // 715 + if (e.name === 'MongoError' || e.name === 'MinimongoError') { // 716 + throw new Meteor.Error(409, e.toString()); // 717 + } else { // 718 + throw e; // 719 + } // 720 + } // 721 + }; // 722 + }); // 723 + // Minimongo on the server gets no stubs; instead, by default // 724 + // it wait()s until its result is ready, yielding. // 725 + // This matches the behavior of macromongo on the server better. // 726 + if (Meteor.isClient || self._connection === Meteor.server) // 727 + self._connection.methods(m); // 728 + } // 729 +}; // 730 + // 731 + // 732 +Meteor.Collection.prototype._updateFetch = function (fields) { // 733 + var self = this; // 734 + // 735 + if (!self._validators.fetchAllFields) { // 736 + if (fields) { // 737 + self._validators.fetch = _.union(self._validators.fetch, fields); // 738 + } else { // 739 + self._validators.fetchAllFields = true; // 740 + // clear fetch just to make sure we don't accidentally read it // 741 + self._validators.fetch = null; // 742 + } // 743 + } // 744 +}; // 745 + // 746 +Meteor.Collection.prototype._isInsecure = function () { // 747 + var self = this; // 748 + if (self._insecure === undefined) // 749 + return !!Package.insecure; // 750 + return self._insecure; // 751 +}; // 752 + // 753 +var docToValidate = function (validator, doc, generatedId) { // 754 + var ret = doc; // 755 + if (validator.transform) { // 756 + ret = EJSON.clone(doc); // 757 + // If you set a server-side transform on your collection, then you don't get // 758 + // to tell the difference between "client specified the ID" and "server // 759 + // generated the ID", because transforms expect to get _id. If you want to // 760 + // do that check, you can do it with a specific // 761 + // `C.allow({insert: f, transform: null})` validator. // 762 + if (generatedId !== null) { // 763 + ret._id = generatedId; // 764 + } // 765 + ret = validator.transform(ret); // 766 + } // 767 + return ret; // 768 +}; // 769 + // 770 +Meteor.Collection.prototype._validatedInsert = function (userId, doc, // 771 + generatedId) { // 772 + var self = this; // 773 + // 774 + // call user validators. // 775 + // Any deny returns true means denied. // 776 + if (_.any(self._validators.insert.deny, function(validator) { // 777 + return validator(userId, docToValidate(validator, doc, generatedId)); // 778 + })) { // 779 + throw new Meteor.Error(403, "Access denied"); // 780 + } // 781 + // Any allow returns true means proceed. Throw error if they all fail. // 782 + if (_.all(self._validators.insert.allow, function(validator) { // 783 + return !validator(userId, docToValidate(validator, doc, generatedId)); // 784 + })) { // 785 + throw new Meteor.Error(403, "Access denied"); // 786 + } // 787 + // 788 + // If we generated an ID above, insert it now: after the validation, but // 789 + // before actually inserting. // 790 + if (generatedId !== null) // 791 + doc._id = generatedId; // 792 + // 793 + self._collection.insert.call(self._collection, doc); // 794 +}; // 795 + // 796 +var transformDoc = function (validator, doc) { // 797 + if (validator.transform) // 798 + return validator.transform(doc); // 799 + return doc; // 800 +}; // 801 + // 802 +// Simulate a mongo `update` operation while validating that the access // 803 +// control rules set by calls to `allow/deny` are satisfied. If all // 804 +// pass, rewrite the mongo operation to use $in to set the list of // 805 +// document ids to change ##ValidatedChange // 806 +Meteor.Collection.prototype._validatedUpdate = function( // 807 + userId, selector, mutator, options) { // 808 + var self = this; // 809 + // 810 + options = options || {}; // 811 + // 812 + if (!LocalCollection._selectorIsIdPerhapsAsObject(selector)) // 813 + throw new Error("validated update should be of a single ID"); // 814 + // 815 + // We don't support upserts because they don't fit nicely into allow/deny // 816 + // rules. // 817 + if (options.upsert) // 818 + throw new Meteor.Error(403, "Access denied. Upserts not " + // 819 + "allowed in a restricted collection."); // 820 + // 821 + // compute modified fields // 822 + var fields = []; // 823 + _.each(mutator, function (params, op) { // 824 + if (op.charAt(0) !== '$') { // 825 + throw new Meteor.Error( // 826 + 403, "Access denied. In a restricted collection you can only update documents, not replace them. Use a Mongo update operator, such as '$set'."); + } else if (!_.has(ALLOWED_UPDATE_OPERATIONS, op)) { // 828 + throw new Meteor.Error( // 829 + 403, "Access denied. Operator " + op + " not allowed in a restricted collection."); // 830 + } else { // 831 + _.each(_.keys(params), function (field) { // 832 + // treat dotted fields as if they are replacing their // 833 + // top-level part // 834 + if (field.indexOf('.') !== -1) // 835 + field = field.substring(0, field.indexOf('.')); // 836 + // 837 + // record the field we are trying to change // 838 + if (!_.contains(fields, field)) // 839 + fields.push(field); // 840 + }); // 841 + } // 842 + }); // 843 + // 844 + var findOptions = {transform: null}; // 845 + if (!self._validators.fetchAllFields) { // 846 + findOptions.fields = {}; // 847 + _.each(self._validators.fetch, function(fieldName) { // 848 + findOptions.fields[fieldName] = 1; // 849 + }); // 850 + } // 851 + // 852 + var doc = self._collection.findOne(selector, findOptions); // 853 + if (!doc) // none satisfied! // 854 + return 0; // 855 + // 856 + var factoriedDoc; // 857 + // 858 + // call user validators. // 859 + // Any deny returns true means denied. // 860 + if (_.any(self._validators.update.deny, function(validator) { // 861 + if (!factoriedDoc) // 862 + factoriedDoc = transformDoc(validator, doc); // 863 + return validator(userId, // 864 + factoriedDoc, // 865 + fields, // 866 + mutator); // 867 + })) { // 868 + throw new Meteor.Error(403, "Access denied"); // 869 + } // 870 + // Any allow returns true means proceed. Throw error if they all fail. // 871 + if (_.all(self._validators.update.allow, function(validator) { // 872 + if (!factoriedDoc) // 873 + factoriedDoc = transformDoc(validator, doc); // 874 + return !validator(userId, // 875 + factoriedDoc, // 876 + fields, // 877 + mutator); // 878 + })) { // 879 + throw new Meteor.Error(403, "Access denied"); // 880 + } // 881 + // 882 + // Back when we supported arbitrary client-provided selectors, we actually // 883 + // rewrote the selector to include an _id clause before passing to Mongo to // 884 + // avoid races, but since selector is guaranteed to already just be an ID, we // 885 + // don't have to any more. // 886 + // 887 + return self._collection.update.call( // 888 + self._collection, selector, mutator, options); // 889 +}; // 890 + // 891 +// Only allow these operations in validated updates. Specifically // 892 +// whitelist operations, rather than blacklist, so new complex // 893 +// operations that are added aren't automatically allowed. A complex // 894 +// operation is one that does more than just modify its target // 895 +// field. For now this contains all update operations except '$rename'. // 896 +// http://docs.mongodb.org/manual/reference/operators/#update // 897 +var ALLOWED_UPDATE_OPERATIONS = { // 898 + $inc:1, $set:1, $unset:1, $addToSet:1, $pop:1, $pullAll:1, $pull:1, // 899 + $pushAll:1, $push:1, $bit:1 // 900 +}; // 901 + // 902 +// Simulate a mongo `remove` operation while validating access control // 903 +// rules. See #ValidatedChange // 904 +Meteor.Collection.prototype._validatedRemove = function(userId, selector) { // 905 + var self = this; // 906 + // 907 + var findOptions = {transform: null}; // 908 + if (!self._validators.fetchAllFields) { // 909 + findOptions.fields = {}; // 910 + _.each(self._validators.fetch, function(fieldName) { // 911 + findOptions.fields[fieldName] = 1; // 912 + }); // 913 + } // 914 + // 915 + var doc = self._collection.findOne(selector, findOptions); // 916 + if (!doc) // 917 + return 0; // 918 + // 919 + // call user validators. // 920 + // Any deny returns true means denied. // 921 + if (_.any(self._validators.remove.deny, function(validator) { // 922 + return validator(userId, transformDoc(validator, doc)); // 923 + })) { // 924 + throw new Meteor.Error(403, "Access denied"); // 925 + } // 926 + // Any allow returns true means proceed. Throw error if they all fail. // 927 + if (_.all(self._validators.remove.allow, function(validator) { // 928 + return !validator(userId, transformDoc(validator, doc)); // 929 + })) { // 930 + throw new Meteor.Error(403, "Access denied"); // 931 + } // 932 + // 933 + // Back when we supported arbitrary client-provided selectors, we actually // 934 + // rewrote the selector to {_id: {$in: [ids that we found]}} before passing to // 935 + // Mongo to avoid races, but since selector is guaranteed to already just be // 936 + // an ID, we don't have to any more. // 937 + // 938 + return self._collection.remove.call(self._collection, selector); // 939 +}; // 940 + // 941 +///////////////////////////////////////////////////////////////////////////////////////////////////////// + +}).call(this); + + +/* Exports */ +if (typeof Package === 'undefined') Package = {}; +Package['mongo-livedata'] = { + MongoInternals: MongoInternals, + MongoTest: MongoTest +}; + +})(); + +//# sourceMappingURL=mongo-livedata.js.map diff --git a/resources/terminal b/resources/terminal new file mode 100755 index 0000000000..5d85fe171d --- /dev/null +++ b/resources/terminal @@ -0,0 +1,50 @@ +#!/bin/sh + +ITERM_EXISTS=`osascript < /dev/null < /dev/null < /dev/null 2>&1; then + cecho "meteorite not found, install using npm install meteorite -g" $red + exit 1 +fi + +if ! type "demeteorizer" > /dev/null 2>&1; then + cecho "Demeteorizer not found, install using npm install demeteorizer -g" $red + exit 1 +fi + +rm -rf ../bundle + +cecho "-----> Building bundle from Meteor app, this may take a few minutes..." $blue +demeteorizer -o ../bundle + +cd ../bundle + +awk '!/fibers/' package.json > temp && mv temp package.json +awk '!/node-aes-gcm/' package.json > temp && mv temp package.json +awk '!/kexec/' package.json > temp && mv temp package.json +awk '!/heapdump/' package.json > temp && mv temp package.json +awk '!/bcrypt/' package.json > temp && mv temp package.json + +NPM="$BASE/resources/cache/node/bin/npm" +NODE="$BASE/resources/cache/node/bin/node" + +cecho "-----> Installing bundle npm packages." $blue +$NPM install + +cecho "-----> Installing custom npm packages." $blue +$NPM install fibers@git+https://github.com/usekite/node-fibers.git --save +$NPM install node-aes-gcm@git+https://github.com/usekite/node-aes-gcm.git --save +$NPM install kexec@git+https://github.com/usekite/node-kexec.git --save +$NPM install heapdump@git+https://github.com/usekite/node-heapdump.git --save +$NPM install bcrypt@0.8.0 --save + +cecho "-----> Removing unnecessary node_modules" $blue +rm -rf ./programs/ctl/node_modules + +cecho "-----> Fixing Fibers for node-webkit" $blue +$NPM install nw-gyp -g +FIBERS_ARCH=$($NODE -p -e 'process.platform + "-" + process.arch + "-v8-" + /[0-9]+\.[0-9]+/.exec(process.versions.v8)[0]') +cd ./node_modules/fibers +$BASE/resources/cache/node/bin/nw-gyp rebuild --target=0.10.2 --arch=x64 +mkdir -p ./bin/$FIBERS_ARCH +cp ./build/Release/fibers.node ./bin/$FIBERS_ARCH/fibers.node + +cecho "Bundle created." $green + +popd diff --git a/script/colors.sh b/script/colors.sh new file mode 100644 index 0000000000..97b77af497 --- /dev/null +++ b/script/colors.sh @@ -0,0 +1,12 @@ +green='\033[00;32m' +purple='\033[00;35m' +blue='\033[00;34m' +red='\033[00;31m' +function cecho { + local default_msg="No message passed." + message=${1:-$default_msg} # Defaults to default message. + color=${2:-$green} + + echo -e -n "$color$message" + echo -e '\033[0m' +} diff --git a/script/dist.sh b/script/dist.sh new file mode 100755 index 0000000000..498cead035 --- /dev/null +++ b/script/dist.sh @@ -0,0 +1,68 @@ +#!/bin/bash +set -e # Auto exit on error + +DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" +source $DIR/colors.sh +source $DIR/versions.sh + +BASE=$DIR/.. +pushd $BASE + +if [ ! -d bundle ]; then + cecho "No bundle, run script/bundle.sh first." $red + exit 1 +fi + +rm -rf dist/osx/Kitematic.app +rm -rf dist/osx/Kitematic.zip +mkdir -p dist/osx/ + +cecho "-----> Creating Kitematic.app..." $blue +cp -R resources/node-webkit/node-webkit.app dist/osx/ +mv dist/osx/node-webkit.app dist/osx/Kitematic.app +mkdir -p dist/osx/Kitematic.app/Contents/Resources/app.nw + +cecho "-----> Copying meteor bundle into Kitematic.app..." $blue +cp -R bundle dist/osx/Kitematic.app/Contents/Resources/app.nw/ + +cecho "-----> Copying node-webkit app into Kitematic.app..." $blue +cp index.html dist/osx/Kitematic.app/Contents/Resources/app.nw/ +cp index.js dist/osx/Kitematic.app/Contents/Resources/app.nw/ +cp package.json dist/osx/Kitematic.app/Contents/Resources/app.nw/ +cp -R node_modules dist/osx/Kitematic.app/Contents/Resources/app.nw/ + +$DIR/setup.sh + +cecho "-----> Copying binary files to Kitematic.app" $blue +mkdir -p dist/osx/Kitematic.app/Contents/Resources/app.nw/resources +cp resources/$BASE_IMAGE_FILE dist/osx/Kitematic.app/Contents/Resources/app.nw/resources/ +cp resources/$VIRTUALBOX_FILE dist/osx/Kitematic.app/Contents/Resources/app.nw/resources/ +cp resources/$COCOASUDO_FILE dist/osx/Kitematic.app/Contents/Resources/app.nw/resources/ +cp resources/$BOOT2DOCKER_CLI_FILE dist/osx/Kitematic.app/Contents/Resources/app.nw/resources/ +cp resources/install dist/osx/Kitematic.app/Contents/Resources/app.nw/resources/ +cp resources/terminal dist/osx/Kitematic.app/Contents/Resources/app.nw/resources/ +cp resources/unison dist/osx/Kitematic.app/Contents/Resources/app.nw/resources/ +cp resources/UNISON_LICENSE.txt dist/osx/Kitematic.app/Contents/Resources/app.nw/resources/ +cp resources/kite-binaries.tar.gz dist/osx/Kitematic.app/Contents/Resources/app.nw/resources/ +cp resources/mongod dist/osx/Kitematic.app/Contents/Resources/app.nw/resources/ +cp resources/MONGOD_LICENSE.txt dist/osx/Kitematic.app/Contents/Resources/app.nw/resources/ +chmod +x dist/osx/Kitematic.app/Contents/Resources/app.nw/resources/$BOOT2DOCKER_CLI_FILE +chmod +x dist/osx/Kitematic.app/Contents/Resources/app.nw/resources/$COCOASUDO_FILE +chmod +x dist/osx/Kitematic.app/Contents/Resources/app.nw/resources/install +chmod +x dist/osx/Kitematic.app/Contents/Resources/app.nw/resources/terminal +chmod +x dist/osx/Kitematic.app/Contents/Resources/app.nw/resources/unison + +if [ -f $DIR/sign.sh ]; then + $DIR/sign.sh $BASE/dist/osx/Kitematic.app +fi + +pushd dist/osx +cecho "-----> Creating disributable zip file...." $blue +zip -rq --display-dots Kitematic.zip Kitematic.app +popd + +cecho "Done." $green +cecho "Kitematic app available at dist/osx/Kitematic.app" $green +cecho "Kitematic zip distribution available at dist/osx/Kitematic.zip" $green + +popd diff --git a/script/run.sh b/script/run.sh new file mode 100755 index 0000000000..da03cc5ab0 --- /dev/null +++ b/script/run.sh @@ -0,0 +1,16 @@ +#!/bin/bash + +DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" +BASE=$DIR/.. + +export ROOT_URL=https://localhost:3000 +export DOCKER_HOST=http://192.168.59.103 +export DOCKER_PORT=2375 + +#export METEOR_SETTINGS=`cat $BASE/meteor/settings_dev.json` +#echo $METEOR_SETTINGS + +cd $BASE/meteor +exec 3< <(mrt --settings $BASE/meteor/settings_dev.json) +sed '/App running at/q' <&3 ; cat <&3 & +NODE_ENV=development $BASE/resources/node-webkit/node-webkit.app/Contents/MacOS/node-webkit $BASE diff --git a/script/setup.sh b/script/setup.sh new file mode 100755 index 0000000000..755cfe9590 --- /dev/null +++ b/script/setup.sh @@ -0,0 +1,75 @@ +#!/bin/bash +set -e # Auto exit on error + +DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" +source $DIR/colors.sh +source $DIR/versions.sh + +BASE=$DIR/.. +pushd $BASE + +mkdir -p resources/cache + +pushd resources/cache + +if [ ! -f $BASE_IMAGE_VERSION_FILE ]; then + cecho "-----> Downloading Kitematic base images..." $purple + curl -L --progress-bar -o $BASE_IMAGE_VERSION_FILE https://s3.amazonaws.com/kite-installer/$BASE_IMAGE_VERSION_FILE +fi + +if [ ! -f $BOOT2DOCKER_CLI_VERSION_FILE ]; then + cecho "-----> Downloading Boot2docker CLI..." $purple + curl -L -o $BOOT2DOCKER_CLI_VERSION_FILE https://github.com/boot2docker/boot2docker-cli/releases/download/v${BOOT2DOCKER_CLI_VERSION}/boot2docker-v${BOOT2DOCKER_CLI_VERSION}-darwin-amd64 +fi + +if [ ! -f kite-node-webkit.tar.gz ]; then + cecho "-----> Downloading node-webkit..." $purple + curl -L -o kite-node-webkit.tar.gz https://s3.amazonaws.com/kite-installer/kite-node-webkit.tar.gz + tar -zxf kite-node-webkit.tar.gz -C .. +fi + +if [ ! -f mongodb-osx-x86_64-2.6.3.tgz ]; then + cecho "-----> Downloading mongodb..." $purple + curl -L -o mongodb-osx-x86_64-2.6.3.tgz http://downloads.mongodb.org/osx/mongodb-osx-x86_64-2.6.3.tgz + tar -zxvf mongodb-osx-x86_64-2.6.3.tgz + cp mongodb-osx-x86_64-2.6.3/bin/mongod .. + cp mongodb-osx-x86_64-2.6.3/GNU-AGPL-3.0 ../MONGOD_LICENSE.txt +fi + +if [ ! -f "node-v0.11.13-darwin-x64.tar.gz" ]; then + cecho "-----> Downloading Nodejs distribution..." $purple + curl -L -o node-v0.11.13-darwin-x64.tar.gz http://nodejs.org/dist/v0.11.13/node-v0.11.13-darwin-x64.tar.gz + mkdir -p node + tar -xzf node-v0.11.13-darwin-x64.tar.gz --strip-components 1 -C node +fi + +popd + +pushd resources + +if [ ! -f $VIRTUALBOX_FILE ]; then + cecho "-----> Downloading virtualbox installer..." $purple + curl -L --progress-bar -o $VIRTUALBOX_FILE https://s3.amazonaws.com/kite-installer/$VIRTUALBOX_FILE +fi + +if [ ! -f $COCOASUDO_FILE ]; then + cecho "-----> Downloading Cocoasudo binary..." $purple + curl -L -o $COCOASUDO_FILE https://github.com/performantdesign/cocoasudo/blob/master/build/Release/cocoasudo + chmod +x $COCOASUDO_FILE +fi + + +cecho "-----> Creating binary files from cache" $blue + +if [ ! -f $BASE_IMAGE_FILE ]; then + cp cache/$BASE_IMAGE_VERSION_FILE $BASE_IMAGE_FILE +fi + +if [ ! -f $BOOT2DOCKER_CLI_FILE ]; then + cp cache/$BOOT2DOCKER_CLI_VERSION_FILE $BOOT2DOCKER_CLI_FILE + chmod +x $BOOT2DOCKER_CLI_FILE +fi + +popd + +popd diff --git a/script/versions.sh b/script/versions.sh new file mode 100644 index 0000000000..0c12c7cd49 --- /dev/null +++ b/script/versions.sh @@ -0,0 +1,11 @@ +BASE_IMAGE_VERSION=0.0.1 +BASE_IMAGE_VERSION_FILE=base-images-$BASE_IMAGE_VERSION.tar.gz +BASE_IMAGE_FILE=base-images.tar.gz + +VIRTUALBOX_FILE=virtualbox-4.3.12.pkg + +BOOT2DOCKER_CLI_VERSION=1.1.2 +BOOT2DOCKER_CLI_VERSION_FILE=boot2docker-$BOOT2DOCKER_CLI_VERSION +BOOT2DOCKER_CLI_FILE=boot2docker + +COCOASUDO_FILE=cocoasudo \ No newline at end of file