diff --git a/CHANGELOG.md b/CHANGELOG.md index beb551644..d227edc7d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,27 @@ +# Xenops release (v2.23.0) + +## Goals + +For this release we wanted to focus on releasing a small set of features built on top of some foundations established in our last release. Since we were able to complete this feature set in less than a week, we decided to bundle them together so users could take benefit from them sooner. + +This work is being funded by the grant we received from NLnet foundation as a part of their PET (Privacy Enhancing Technology) fund. You can read all about this grant on our latest blog post (https://blog.cryptpad.fr/2019/05/27/Our-future-is-collaborative/). + +## Update notes + +* This update only uses clientside dependencies. Fetch the latest code for the core repository, and depending on when you last updated you may need to `bower update` as well. +* User data is "pinned" on CryptPad instances to keep track of what encrypted data can be safely removed. At one point this system was optional and could be disabled by setting `enablePinning = false` in `customize/application_config.js`. At some point we stopped testing whether CryptPad could actually work without pinning enabled, and at this point it is definitely broken. As such, we've decided to drop support for this configuration. + +## Features + +* Some of our multilingual contributors have contributed translations in the German, Russian, and Italian. The history of their contributions is available on our weblate instance (https://weblate.cryptpad.fr/projects/cryptpad/app/). +* This release introduces a practical use-case of the encrypted mailbox infrastructure which we developed in our last release. Registered users are now able to use this system to accept friend requests and review the status of friend requests that have been accepted or declined. Unlike our previous friend request system, our usage of encrypted mailboxes allows for users to send friend requests from other user's profiles whether or not they are online. +* We've also put some time towards improving user profiles as well. When you change your display name from anywhere within CryptPad the name used in your profile will be updated as well. We've also made updates to other users' profiles render in real-time, since the rest of CryptPad generally updates instantly. + +## Bug fixes + +* Some small components of CryptPad time out if they don't work within a set amount of time, and apparently this timeout was causing problems in the newest Tor browser version. We've drastically increased the timeout to make it less likely to cause problems when loading very large documents. +* We realized that Weblate was committing "empty strings" to our translation files. Our internationalization system was configured to fall back to the English translation if no translation was available in the user's preferred language, but these empty strings fooled the system into displaying nothing instead. We addressed the issue by checking whether a string was really present, and not just whether a value existed. + # Wolf release (v2.22.0) ## Goals diff --git a/Dockerfile b/Dockerfile index 974c10928..7378b14ed 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,9 +1,26 @@ -# 6-stretch is the ONLY node 6 release supported by arm32v7, arm64v8 and x86-64 docker hub labels -FROM node:6-stretch +# We use multi stage builds +FROM node:6-stretch-slim AS build + +RUN apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -yq git jq python +RUN npm install -g bower + +# install tini in this stage to avoid the need of jq and python +# in the final image +ADD docker-install-tini.sh /usr/local/bin/docker-install-tini.sh +RUN /usr/local/bin/docker-install-tini.sh + +COPY . /cryptpad +WORKDIR /cryptpad + +RUN npm install --production \ + && npm install -g bower \ + && bower install --allow-root + +FROM node:6-stretch-slim # You want USE_SSL=true if not putting cryptpad behind a proxy ENV USE_SSL=false -ENV STORAGE=\'./storage/file\' +ENV STORAGE="'./storage/file'" ENV LOG_TO_STDOUT=true # Persistent storage needs @@ -17,36 +34,14 @@ VOLUME /cryptpad/block VOLUME /cryptpad/blob VOLUME /cryptpad/blobstage -# Required packages -# jq is a build only dependency, removed in cleanup stage -RUN apt-get update \ - && apt-get install -y --no-install-recommends \ - git jq python - -# Install tini for faux init -# sleep 1 is to ensure overlay2 can catch up with the copy prior to running chmod -COPY ./docker-install-tini.sh / -RUN chmod a+x /docker-install-tini.sh \ - && sleep 1 \ - && /docker-install-tini.sh \ - && rm /docker-install-tini.sh - -# Cleanup apt -RUN apt-get remove -y --purge jq python \ - && apt-get auto-remove -y \ - && apt-get clean \ - && rm -rf /var/lib/apt/lists/* - -# Install cryptpad -COPY . /cryptpad +# Copy cryptpad and tini from the build container +COPY --from=build /sbin/tini /sbin/tini +COPY --from=build /cryptpad /cryptpad + WORKDIR /cryptpad -RUN npm install --production \ - && npm install -g bower \ - && bower install --allow-root # Unsafe / Safe ports EXPOSE 3000 3001 # Run cryptpad on startup -CMD ["/sbin/tini", "--", "/cryptpad/container-start.sh"] - +CMD ["/sbin/tini", "--", "/cryptpad/container-start.sh"] \ No newline at end of file diff --git a/bower.json b/bower.json index 1021dbf62..b362c0a5f 100644 --- a/bower.json +++ b/bower.json @@ -45,7 +45,7 @@ "localforage": "^1.5.2", "html2canvas": "^0.4.1", "croppie": "^2.5.0", - "sortablejs": "#^1.6.0", + "sortablejs": "^1.6.0", "saferphore": "^0.0.1", "jszip": "Stuk/jszip#^3.1.5", "requirejs-plugins": "^1.0.3" diff --git a/config/config.example.js b/config/config.example.js index b896a0363..eb134591c 100644 --- a/config/config.example.js +++ b/config/config.example.js @@ -219,6 +219,29 @@ module.exports = { */ inactiveTime: 90, // days + /* CryptPad can be configured to remove inactive data which has not been pinned. + * Deletion of data is always risky and as an operator you have the choice to + * archive data instead of deleting it outright. Set this value to true if + * you want your server to archive files and false if you want to keep using + * the old behaviour of simply removing files. + * + * WARNING: this is not implemented universally, so at the moment this will + * only apply to the removal of 'channels' due to inactivity. + */ + retainData: true, + + /* As described above, CryptPad offers the ability to archive some data + * instead of deleting it outright. This archived data still takes up space + * and so you'll probably still want to remove these files after a brief period. + * The intent with this feature is to provide a safety net in case of accidental + * deletion. Set this value to the number of days you'd like to retain + * archived data before it's removed permanently. + * + * If 'retainData' is set to false, there will never be any archived data + * to remove. + */ + archiveRetentionTime: 15, + /* Max Upload Size (bytes) * this sets the maximum size of any one file uploaded to the server. * anything larger than this size will be rejected @@ -245,12 +268,21 @@ module.exports = { * ===================== */ /* - CryptPad stores each document in an individual file on your hard drive. - Specify a directory where files should be stored. - It will be created automatically if it does not already exist. - */ + * CryptPad stores each document in an individual file on your hard drive. + * Specify a directory where files should be stored. + * It will be created automatically if it does not already exist. + */ filePath: './datastore/', + /* CryptPad offers the ability to archive data for a configurable period + * before deleting it, allowing a means of recovering data in the event + * that it was deleted accidentally. + * + * To set the location of this archive directory to a custom value, change + * the path below: + */ + archivePath: './data/archive', + /* CryptPad allows logged in users to request that particular documents be * stored by the server indefinitely. This is called 'pinning'. * Pin requests are stored in a pin-store. The location of this store is diff --git a/customize.dist/fonts/cptools/README.md b/customize.dist/fonts/cptools/README.md new file mode 100644 index 000000000..e9307919f --- /dev/null +++ b/customize.dist/fonts/cptools/README.md @@ -0,0 +1,3 @@ +The "cptools" font is using the [palette-solid icon](https://fontawesome.com/icons/palette?style=solid) from the "Font Awesome 5 Free" icon set. + +This icon is licensed under the [Creative Commons Attribution 4.0 International license](https://fontawesome.com/license/free). diff --git a/customize.dist/fonts/cptools/fonts/cptools.svg b/customize.dist/fonts/cptools/fonts/cptools.svg index b2839371d..7c2b30e3f 100644 --- a/customize.dist/fonts/cptools/fonts/cptools.svg +++ b/customize.dist/fonts/cptools/fonts/cptools.svg @@ -23,4 +23,5 @@ + \ No newline at end of file diff --git a/customize.dist/fonts/cptools/fonts/cptools.ttf b/customize.dist/fonts/cptools/fonts/cptools.ttf index 3dfa366fc..235332901 100644 Binary files a/customize.dist/fonts/cptools/fonts/cptools.ttf and b/customize.dist/fonts/cptools/fonts/cptools.ttf differ diff --git a/customize.dist/fonts/cptools/fonts/cptools.woff b/customize.dist/fonts/cptools/fonts/cptools.woff index 93fc56f2e..dc8f88640 100644 Binary files a/customize.dist/fonts/cptools/fonts/cptools.woff and b/customize.dist/fonts/cptools/fonts/cptools.woff differ diff --git a/customize.dist/fonts/cptools/style.css b/customize.dist/fonts/cptools/style.css index af5b4c4e1..a25519ab4 100644 --- a/customize.dist/fonts/cptools/style.css +++ b/customize.dist/fonts/cptools/style.css @@ -1,9 +1,9 @@ @font-face { font-family: 'cptools'; src: - url('fonts/cptools.ttf?703wg5') format('truetype'), - url('fonts/cptools.woff?703wg5') format('woff'), - url('fonts/cptools.svg?703wg5#cptools') format('svg'); + url('fonts/cptools.ttf?gjk60v') format('truetype'), + url('fonts/cptools.woff?gjk60v') format('woff'), + url('fonts/cptools.svg?gjk60v#cptools') format('svg'); font-weight: normal; font-style: normal; } @@ -24,6 +24,9 @@ -moz-osx-font-smoothing: grayscale; } +.cptools-palette:before { + content: "\e910"; +} .cptools-slide:before { content: "\e902"; } diff --git a/customize.dist/messages.js b/customize.dist/messages.js index 68b6083d6..80fffddd0 100755 --- a/customize.dist/messages.js +++ b/customize.dist/messages.js @@ -1,6 +1,7 @@ (function () { // add your module to this map so it gets used var map = { + 'ca': 'Català', 'de': 'Deutsch', 'es': 'Español', 'el': 'Ελληνικά', @@ -12,6 +13,7 @@ var map = { 'ro': 'Română', 'ru': 'Русский', 'zh': '繁體中文', + 'te': 'తెలుగు', }; var messages = {}; @@ -69,10 +71,25 @@ define(req, function(Util, AppConfig, Default, Language) { }); } - Util.extend(messages, Default); + var extend = function (a, b) { + for (var k in b) { + if (Util.isObject(b[k])) { + a[k] = Util.isObject(a[k]) ? a[k] : {}; + extend(a[k], b[k]); + continue; + } + if (Array.isArray(b[k])) { + a[k] = b[k].slice(); + continue; + } + a[k] = b[k] || a[k]; + } + }; + + extend(messages, Default); if (Language && language !== defaultLanguage) { // Add the translated keys to the returned object - Util.extend(messages, Language); + extend(messages, Language); } messages._languages = map; diff --git a/customize.dist/src/less2/include/alertify.less b/customize.dist/src/less2/include/alertify.less index 0df305ddc..ec531e6b0 100644 --- a/customize.dist/src/less2/include/alertify.less +++ b/customize.dist/src/less2/include/alertify.less @@ -144,6 +144,10 @@ padding: @alertify_padding-base; background: #fff; box-shadow: @alertify_box-shadow; + &.wide { + width: 1000px; + max-width: 70%; + } } .msg { diff --git a/customize.dist/src/less2/include/help.less b/customize.dist/src/less2/include/help.less index a094f28ac..2859b42fa 100644 --- a/customize.dist/src/less2/include/help.less +++ b/customize.dist/src/less2/include/help.less @@ -33,6 +33,7 @@ position: absolute; top: 5px; right: 5px; + cursor: pointer; } .cp-help-text { color: @help-text-color; diff --git a/customize.dist/src/less2/include/notifications.less b/customize.dist/src/less2/include/notifications.less index a65870905..4b37c14b4 100644 --- a/customize.dist/src/less2/include/notifications.less +++ b/customize.dist/src/less2/include/notifications.less @@ -10,7 +10,7 @@ display: flex; flex-flow: column; .cp-notification { - height: @notif-height; + min-height: @notif-height; display: flex; .cp-notification-content { flex: 1; @@ -32,8 +32,9 @@ display: none; align-items: center; justify-content: center; - span { - cursor: pointer; + cursor: pointer; + &:hover { + background-color: rgba(0,0,0,0.1); } } } diff --git a/customize.dist/src/less2/include/share.less b/customize.dist/src/less2/include/share.less new file mode 100644 index 000000000..98641a5f5 --- /dev/null +++ b/customize.dist/src/less2/include/share.less @@ -0,0 +1,86 @@ +@import (reference) "./colortheme-all.less"; +@import (reference) './modal.less'; +@import (reference) './alertify.less'; +@import (reference) './avatar.less'; +@import (reference) './checkmark.less'; +@import (reference) './password-input.less'; +@import (reference) "./tools.less"; +.share_main () { + .alertify_main(); + .checkmark_main(20px); + .password_main(); + .modal_main(); + + .cp-share-columns { + display: flex; + flex-flow: row; + + .cp-share-column { + width: 50%; + padding: 0 10px; + } + } + .cp-share-grid, .cp-share-list { + .avatar_main(50px); + display: flex; + justify-content: space-between; + flex-wrap: wrap; + } + .cp-share-list { + margin-bottom: 15px; + } + .cp-share-grid { + max-height: 228px; + overflow-x: auto; + } + .cp-recent-only { + .cp-share-grid, .cp-share-grid-filter { + display: none; + } + } + .cp-share-grid-filter { + display: flex; + input { + flex: 1; + min-width: 0; + margin-bottom: 0 !important; + &::placeholder { /* Chrome, Firefox, Opera, Safari 10.1+ */ + color: @colortheme_alertify-primary-text; + opacity: 1; /* Firefox */ + } + } + margin-bottom: 15px; + } + .cp-share-friend { + width: 70px; + height: 70px; + display: flex; + flex-flow: column; + justify-content: center; + align-items: center; + padding: 5px; + margin-bottom: 6px; + cursor: default; + transition: order 0.5s, background-color 0.5s; + .tools_unselectable(); + + &.cp-selected { + background-color: @colortheme_alertify-primary; + color: @colortheme_alertify-primary-text; + order: -1 !important; + } + .cp-share-friend-name { + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + width: 100%; + text-align: center; + } + border: 1px solid @colortheme_alertify-primary; + &.cp-fake-friend { + visibility: hidden; + } + } +} + + diff --git a/customize.dist/src/outer.css b/customize.dist/src/outer.css new file mode 100644 index 000000000..b36fb2c10 --- /dev/null +++ b/customize.dist/src/outer.css @@ -0,0 +1,15 @@ +html, body { + margin: 0px; + padding: 0px; +} +#sbox-iframe, #sbox-share-iframe, #sbox-filePicker-iframe { + position: fixed; + top:0; left:0; + bottom:0; right:0; + width:100%; + height: 100%; + border: 0; + margin:0; + padding:0; + overflow:hidden; +} diff --git a/customize.dist/translations/README.md b/customize.dist/translations/README.md index 05ddc2b34..66bacffea 100644 --- a/customize.dist/translations/README.md +++ b/customize.dist/translations/README.md @@ -17,7 +17,7 @@ Translations can now be made using [Weblate](https://weblate.cryptpad.fr). We ma ## Translate an existing language -* All translations can be done using the Weblate UI. For better help about how to use the tool, please check the [Weblate documentation](https://docs.weblate.org/en/latest/user/index.html). +* All translations can be done using the Weblate UI. For better help about how to use the tool, please check the [Weblate documentation](https://docs.weblate.org/en/latest/). * Our Weblate instance is configured to always require approval for changes. ### Update an existing translation diff --git a/customize.dist/translations/messages.ca.js b/customize.dist/translations/messages.ca.js new file mode 100644 index 000000000..5e5b2a39f --- /dev/null +++ b/customize.dist/translations/messages.ca.js @@ -0,0 +1,14 @@ +/* + * You can override the translation text using this file. + * The recommended method is to make a copy of this file (/customize.dist/translations/messages.{LANG}.js) + in a 'customize' directory (/customize/translations/messages.{LANG}.js). + * If you want to check all the existing translation keys, you can open the internal language file + but you should not change it directly (/common/translations/messages.{LANG}.js) +*/ +define(['/common/translations/messages.ca.js'], function (Messages) { + // Replace the existing keys in your copied file here: + // Messages.button_newpad = "New Rich Text Document"; + + return Messages; +}); + diff --git a/customize.dist/translations/messages.te.js b/customize.dist/translations/messages.te.js new file mode 100644 index 000000000..2cabce54b --- /dev/null +++ b/customize.dist/translations/messages.te.js @@ -0,0 +1,14 @@ +/* + * You can override the translation text using this file. + * The recommended method is to make a copy of this file (/customize.dist/translations/messages.{LANG}.js) + in a 'customize' directory (/customize/translations/messages.{LANG}.js). + * If you want to check all the existing translation keys, you can open the internal language file + but you should not change it directly (/common/translations/messages.{LANG}.js) +*/ +define(['/common/translations/messages.te.js'], function (Messages) { + // Replace the existing keys in your copied file here: + // Messages.button_newpad = "New Rich Text Document"; + + return Messages; +}); + diff --git a/docs/cryptpad-docker.md b/docs/cryptpad-docker.md index 2647da319..12018902e 100644 --- a/docs/cryptpad-docker.md +++ b/docs/cryptpad-docker.md @@ -21,7 +21,7 @@ Run from the cryptpad source directory, keeping instance state in `/var/cryptpad docker build -t xwiki/cryptpad . docker run --restart=always -d --name cryptpad -p 3000:3000 -p 3001:3001 \ -v /var/cryptpad/files:/cryptpad/datastore \ --v /var/cryptpad/customize:/cryptpad/customize +-v /var/cryptpad/customize:/cryptpad/customize \ -v /var/cryptpad/blob:/cryptpad/blob \ -v /var/cryptpad/blobstage:/cryptpad/blobstage \ -v /var/cryptpad/pins:/cryptpad/pins \ diff --git a/lib/load-config.js b/lib/load-config.js index 7b9f73251..80f4706dc 100644 --- a/lib/load-config.js +++ b/lib/load-config.js @@ -1,11 +1,14 @@ +/* jslint node: true */ +"use strict"; var config; +var configPath = process.env.CRYPTPAD_CONFIG || "../config/config"; try { - config = require("../config/config"); + config = require(configPath); if (config.adminEmail === 'i.did.not.read.my.config@cryptpad.fr') { console.log("You can configure the administrator email (adminEmail) in your config/config.js file"); } } catch (e) { - console.log("You can customize the configuration by copying config/config.example.js to config/config.js"); + console.log("Config not found, loading the example config. You can customize the configuration by copying config/config.example.js to " + configPath); config = require("../config/config.example"); } module.exports = config; diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 000000000..30b43f03e --- /dev/null +++ b/package-lock.json @@ -0,0 +1,1304 @@ +{ + "name": "cryptpad", + "version": "2.23.0", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "accepts": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz", + "integrity": "sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA==", + "requires": { + "mime-types": "~2.1.24", + "negotiator": "0.6.2" + } + }, + "ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", + "dev": true + }, + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "^1.9.0" + } + }, + "array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=" + }, + "array-union": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-1.0.2.tgz", + "integrity": "sha1-mjRBDk9OPaI96jdb5b5w8kd47Dk=", + "dev": true, + "requires": { + "array-uniq": "^1.0.1" + } + }, + "array-uniq": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/array-uniq/-/array-uniq-1.0.3.tgz", + "integrity": "sha1-r2rId6Jcx/dOBYiUdThY39sk/bY=", + "dev": true + }, + "asap": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", + "integrity": "sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY=", + "dev": true, + "optional": true + }, + "async-limiter": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.0.tgz", + "integrity": "sha512-jp/uFnooOiO+L211eZOoSyzpOITMXx1rBITauYykG3BRYPu8h0UcxsPNB04RR5vo4Tyz3+ay17tR6JVf9qzYWg==" + }, + "balanced-match": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", + "dev": true + }, + "body-parser": { + "version": "1.18.3", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.18.3.tgz", + "integrity": "sha1-WykhmP/dVTs6DyDe0FkrlWlVyLQ=", + "requires": { + "bytes": "3.0.0", + "content-type": "~1.0.4", + "debug": "2.6.9", + "depd": "~1.1.2", + "http-errors": "~1.6.3", + "iconv-lite": "0.4.23", + "on-finished": "~2.3.0", + "qs": "6.5.2", + "raw-body": "2.3.3", + "type-is": "~1.6.16" + } + }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "bytes": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", + "integrity": "sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg=" + }, + "chainpad-server": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/chainpad-server/-/chainpad-server-3.0.2.tgz", + "integrity": "sha512-c5aEljVAapDKKs0+Rt2jymKAszm8X4ZeLFNJj1yxflwBqoh0jr8OANYvbfjtNaYFe2Wdflp/1i4gibYX4IMc+g==", + "requires": { + "nthen": "^0.1.8", + "pull-stream": "^3.6.9", + "stream-to-pull-stream": "^1.7.3", + "tweetnacl": "~0.12.2", + "ws": "^3.3.1" + } + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "cli": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/cli/-/cli-1.0.1.tgz", + "integrity": "sha1-IoF1NPJL+klQw01TLUjsvGIbjBQ=", + "dev": true, + "requires": { + "exit": "0.1.2", + "glob": "^7.1.1" + } + }, + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", + "dev": true + }, + "commander": { + "version": "2.20.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.0.tgz", + "integrity": "sha512-7j2y+40w61zy6YC2iRNpUe/NwhNyoXrYpHMrSunaMG64nRnaf96zO/KMQR4OyN/UnE5KLyEBnKHd4aG3rskjpQ==", + "dev": true + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", + "dev": true + }, + "console-browserify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/console-browserify/-/console-browserify-1.1.0.tgz", + "integrity": "sha1-8CQcRXMKn8YyOyBtvzjtx0HQuxA=", + "dev": true, + "requires": { + "date-now": "^0.1.4" + } + }, + "content-disposition": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.2.tgz", + "integrity": "sha1-DPaLud318r55YcOoUXjLhdunjLQ=" + }, + "content-type": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", + "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==" + }, + "cookie": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.3.1.tgz", + "integrity": "sha1-5+Ch+e9DtMi6klxcWpboBtFoc7s=" + }, + "cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=" + }, + "core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=", + "dev": true + }, + "date-now": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/date-now/-/date-now-0.1.4.tgz", + "integrity": "sha1-6vQ5/U1ISK105cx9vvIAZyueNFs=", + "dev": true + }, + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + } + }, + "depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=" + }, + "destroy": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", + "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=" + }, + "dir-glob": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-2.2.2.tgz", + "integrity": "sha512-f9LBi5QWzIW3I6e//uxZoLBlUt9kcp66qo0sSCxL6YZKc75R1c4MFCoe/LaZiBGmgujvQdxc5Bn3QhfyvK5Hsw==", + "dev": true, + "requires": { + "path-type": "^3.0.0" + } + }, + "dom-serializer": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.1.1.tgz", + "integrity": "sha512-l0IU0pPzLWSHBcieZbpOKgkIn3ts3vAh7ZuFyXNwJxJXk/c4Gwj9xaTJwIDVQCXawWD0qb3IzMGH5rglQaO0XA==", + "dev": true, + "requires": { + "domelementtype": "^1.3.0", + "entities": "^1.1.1" + }, + "dependencies": { + "entities": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/entities/-/entities-1.1.2.tgz", + "integrity": "sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w==", + "dev": true + } + } + }, + "domelementtype": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.3.1.tgz", + "integrity": "sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w==", + "dev": true + }, + "domhandler": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-2.3.0.tgz", + "integrity": "sha1-LeWaCCLVAn+r/28DLCsloqir5zg=", + "dev": true, + "requires": { + "domelementtype": "1" + } + }, + "domutils": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-1.5.1.tgz", + "integrity": "sha1-3NhIiib1Y9YQeeSMn3t+Mjc2gs8=", + "dev": true, + "requires": { + "dom-serializer": "0", + "domelementtype": "1" + } + }, + "dot-prop": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-4.2.0.tgz", + "integrity": "sha512-tUMXrxlExSW6U2EXiiKGSBVdYgtV8qlHL+C10TsW4PURY/ic+eaysnSkwB4kA/mBlCyy/IKDJ+Lc3wbWeaXtuQ==", + "dev": true, + "requires": { + "is-obj": "^1.0.0" + } + }, + "ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=" + }, + "encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=" + }, + "entities": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-1.0.0.tgz", + "integrity": "sha1-sph6o4ITR/zeZCsk/fyeT7cSvyY=", + "dev": true + }, + "errno": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/errno/-/errno-0.1.7.tgz", + "integrity": "sha512-MfrRBDWzIWifgq6tJj60gkAwtLNb6sQPlcFrSOflcP1aFmmruKQ2wRnze/8V6kgyz7H3FF8Npzv78mZ7XLLflg==", + "dev": true, + "optional": true, + "requires": { + "prr": "~1.0.1" + } + }, + "escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=" + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", + "dev": true + }, + "etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=" + }, + "exit": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", + "integrity": "sha1-BjJjj42HfMghB9MKD/8aF8uhzQw=", + "dev": true + }, + "express": { + "version": "4.16.4", + "resolved": "https://registry.npmjs.org/express/-/express-4.16.4.tgz", + "integrity": "sha512-j12Uuyb4FMrd/qQAm6uCHAkPtO8FDTRJZBDd5D2KOL2eLaz1yUNdUB/NOIyq0iU4q4cFarsUCrnFDPBcnksuOg==", + "requires": { + "accepts": "~1.3.5", + "array-flatten": "1.1.1", + "body-parser": "1.18.3", + "content-disposition": "0.5.2", + "content-type": "~1.0.4", + "cookie": "0.3.1", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "~1.1.2", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.1.1", + "fresh": "0.5.2", + "merge-descriptors": "1.0.1", + "methods": "~1.1.2", + "on-finished": "~2.3.0", + "parseurl": "~1.3.2", + "path-to-regexp": "0.1.7", + "proxy-addr": "~2.0.4", + "qs": "6.5.2", + "range-parser": "~1.2.0", + "safe-buffer": "5.1.2", + "send": "0.16.2", + "serve-static": "1.13.2", + "setprototypeof": "1.1.0", + "statuses": "~1.4.0", + "type-is": "~1.6.16", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + } + }, + "finalhandler": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.1.tgz", + "integrity": "sha512-Y1GUDo39ez4aHAw7MysnUD5JzYX+WaIj8I57kO3aEPT1fFRL4sr7mjei97FgnwhAyyzRYmQZaTHb2+9uZ1dPtg==", + "requires": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "on-finished": "~2.3.0", + "parseurl": "~1.3.2", + "statuses": "~1.4.0", + "unpipe": "~1.0.0" + } + }, + "flatten": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/flatten/-/flatten-1.0.2.tgz", + "integrity": "sha1-2uRqnXj74lKSJYzB54CkHZXAN4I=", + "dev": true + }, + "flow-bin": { + "version": "0.59.0", + "resolved": "https://registry.npmjs.org/flow-bin/-/flow-bin-0.59.0.tgz", + "integrity": "sha512-yJDRffvby5mCTkbwOdXwiGDjeea8Z+BPVuP53/tHqHIZC+KtQD790zopVf7mHk65v+wRn+TZ7tkRSNA9oDmyLg==", + "dev": true + }, + "forwarded": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz", + "integrity": "sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ=" + }, + "fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=" + }, + "fs-extra": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz", + "integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==", + "requires": { + "graceful-fs": "^4.1.2", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + } + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", + "dev": true + }, + "gar": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/gar/-/gar-1.0.4.tgz", + "integrity": "sha512-w4n9cPWyP7aHxKxYHFQMegj7WIAsL/YX/C4Bs5Rr8s1H9M1rNtRWRsw+ovYMkXDQ5S4ZbYHsHAPmevPjPgw44w==" + }, + "get-folder-size": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/get-folder-size/-/get-folder-size-2.0.1.tgz", + "integrity": "sha512-+CEb+GDCM7tkOS2wdMKTn9vU7DgnKUTuDlehkNJKNSovdCOVxs14OfKCk4cvSaR3za4gj+OBdl9opPN9xrJ0zA==", + "requires": { + "gar": "^1.0.4", + "tiny-each-async": "2.0.3" + } + }, + "glob": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.4.tgz", + "integrity": "sha512-hkLPepehmnKk41pUGm3sYxoFs/umurYfYJCerbXEyFIWcAzvpipAgVkBqqT9RBKMGjnq6kMuyYwha6csxbiM1A==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "globby": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/globby/-/globby-7.1.1.tgz", + "integrity": "sha1-+yzP+UAfhgCUXfral0QMypcrhoA=", + "dev": true, + "requires": { + "array-union": "^1.0.1", + "dir-glob": "^2.0.0", + "glob": "^7.1.2", + "ignore": "^3.3.5", + "pify": "^3.0.0", + "slash": "^1.0.0" + } + }, + "graceful-fs": { + "version": "4.1.15", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.15.tgz", + "integrity": "sha512-6uHUhOPEBgQ24HM+r6b/QwWfZq+yiFcipKFrOFiBEnWdy5sdzYoi+pJeQaPI5qOLRFqWmAXUPQNsielzdLoecA==" + }, + "has-ansi": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", + "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=", + "dev": true, + "requires": { + "ansi-regex": "^2.0.0" + } + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "dev": true + }, + "htmlparser2": { + "version": "3.8.3", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-3.8.3.tgz", + "integrity": "sha1-mWwosZFRaovoZQGn15dX5ccMEGg=", + "dev": true, + "requires": { + "domelementtype": "1", + "domhandler": "2.3", + "domutils": "1.5", + "entities": "1.0", + "readable-stream": "1.1" + } + }, + "http-errors": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz", + "integrity": "sha1-i1VoC7S+KDoLW/TqLjhYC+HZMg0=", + "requires": { + "depd": "~1.1.2", + "inherits": "2.0.3", + "setprototypeof": "1.1.0", + "statuses": ">= 1.4.0 < 2" + } + }, + "iconv-lite": { + "version": "0.4.23", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.23.tgz", + "integrity": "sha512-neyTUVFtahjf0mB3dZT77u+8O0QB89jFdnBkd5P1JgYPbPaia3gXXOVL2fq8VyU2gMMD7SaN7QukTB/pmXYvDA==", + "requires": { + "safer-buffer": ">= 2.1.2 < 3" + } + }, + "ignore": { + "version": "3.3.10", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-3.3.10.tgz", + "integrity": "sha512-Pgs951kaMm5GXP7MOvxERINe3gsaVjUWFm+UZPSq9xYriQAksyhg0csnS0KXSNRD5NmNdapXEpjxG49+AKh/ug==", + "dev": true + }, + "image-size": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/image-size/-/image-size-0.5.5.tgz", + "integrity": "sha1-Cd/Uq50g4p6xw+gLiZA3jfnjy5w=", + "dev": true, + "optional": true + }, + "immediate": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", + "integrity": "sha1-nbHb0Pr43m++D13V5Wu2BigN5ps=", + "dev": true + }, + "indexes-of": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/indexes-of/-/indexes-of-1.0.1.tgz", + "integrity": "sha1-8w9xbI4r00bHtn0985FVZqfAVgc=", + "dev": true + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "dev": true, + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" + }, + "ipaddr.js": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.0.tgz", + "integrity": "sha512-M4Sjn6N/+O6/IXSJseKqHoFc+5FdGJ22sXqnjTpdZweHK64MzEPAyQZyEU3R/KRv2GLoa7nNtg/C2Ev6m7z+eA==" + }, + "is-obj": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-1.0.1.tgz", + "integrity": "sha1-PkcprB9f3gJc19g6iW2rn09n2w8=", + "dev": true + }, + "isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=", + "dev": true + }, + "js-base64": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/js-base64/-/js-base64-2.5.1.tgz", + "integrity": "sha512-M7kLczedRMYX4L8Mdh4MzyAMM9O5osx+4FcOQuTvr3A9F2D9S5JXheN0ewNbrvK2UatkTRhL5ejGmGSjNMiZuw==", + "dev": true + }, + "jshint": { + "version": "2.9.7", + "resolved": "https://registry.npmjs.org/jshint/-/jshint-2.9.7.tgz", + "integrity": "sha512-Q8XN38hGsVQhdlM+4gd1Xl7OB1VieSuCJf+fEJjpo59JH99bVJhXRXAh26qQ15wfdd1VPMuDWNeSWoNl53T4YA==", + "dev": true, + "requires": { + "cli": "~1.0.0", + "console-browserify": "1.1.x", + "exit": "0.1.x", + "htmlparser2": "3.8.x", + "lodash": "~4.17.10", + "minimatch": "~3.0.2", + "shelljs": "0.3.x", + "strip-json-comments": "1.0.x" + } + }, + "jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=", + "requires": { + "graceful-fs": "^4.1.6" + } + }, + "jszip": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.2.1.tgz", + "integrity": "sha512-iCMBbo4eE5rb1VCpm5qXOAaUiRKRUKiItn8ah2YQQx9qymmSAY98eyQfioChEYcVQLh0zxJ3wS4A0mh90AVPvw==", + "dev": true, + "requires": { + "lie": "~3.3.0", + "pako": "~1.0.2", + "readable-stream": "~2.3.6", + "set-immediate-shim": "~1.0.1" + }, + "dependencies": { + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", + "dev": true + }, + "readable-stream": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", + "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", + "dev": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "requires": { + "safe-buffer": "~5.1.0" + } + } + } + }, + "less": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/less/-/less-2.7.1.tgz", + "integrity": "sha1-bL/qIrO4MDBOml+zcdVPpIDJ188=", + "dev": true, + "requires": { + "errno": "^0.1.1", + "graceful-fs": "^4.1.2", + "image-size": "~0.5.0", + "mime": "^1.2.11", + "mkdirp": "^0.5.0", + "promise": "^7.1.1", + "source-map": "^0.5.3" + } + }, + "lesshint": { + "version": "4.6.5", + "resolved": "https://registry.npmjs.org/lesshint/-/lesshint-4.6.5.tgz", + "integrity": "sha512-aTKYRG498nnVWDCAHdb9dRHdp7gP92Fa5rKdMFt6z3HfyxB2EkO8ce3hekn3E2/TR4+ROWcNL4M5ot8SpC0slg==", + "dev": true, + "requires": { + "commander": "^2.8.0", + "globby": "^7.0.0", + "lodash.merge": "^4.0.1", + "lodash.sortby": "^4.0.1", + "minimatch": "^3.0.2", + "postcss": "^6.0.0", + "postcss-less": "^1.1.3", + "postcss-selector-parser": "^3.0.0", + "postcss-values-parser": "^1.3.1", + "rcfinder": "^0.1.8", + "strip-json-comments": "^2.0.0" + }, + "dependencies": { + "strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=", + "dev": true + } + } + }, + "lex": { + "version": "1.7.9", + "resolved": "https://registry.npmjs.org/lex/-/lex-1.7.9.tgz", + "integrity": "sha1-XVY2zO9XQ0g2KTi3mkfw7tjtDUM=" + }, + "lie": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", + "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", + "dev": true, + "requires": { + "immediate": "~3.0.5" + } + }, + "lodash": { + "version": "4.17.11", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz", + "integrity": "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg==", + "dev": true + }, + "lodash.clonedeep": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", + "integrity": "sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8=", + "dev": true + }, + "lodash.merge": { + "version": "4.6.1", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.1.tgz", + "integrity": "sha512-AOYza4+Hf5z1/0Hztxpm2/xiPZgi/cjMqdnKTUWTBSKchJlxXXuUSxCCl8rJlf4g6yww/j6mA8nC8Hw/EZWxKQ==", + "dev": true + }, + "lodash.sortby": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz", + "integrity": "sha1-7dFMgk4sycHgsKG0K7UhBRakJDg=", + "dev": true + }, + "looper": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/looper/-/looper-3.0.0.tgz", + "integrity": "sha1-LvpUw7HLq6m5Su4uWRSwvlf7t0k=" + }, + "media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=" + }, + "merge-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", + "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=" + }, + "methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=" + }, + "mime": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.4.1.tgz", + "integrity": "sha512-KI1+qOZu5DcW6wayYHSzR/tXKCDC5Om4s1z2QJjDULzLcmf3DvzS7oluY4HCTrc+9FiKmWUgeNLg7W3uIQvxtQ==" + }, + "mime-db": { + "version": "1.40.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.40.0.tgz", + "integrity": "sha512-jYdeOMPy9vnxEqFRRo6ZvTZ8d9oPb+k18PKoYNYUe2stVEBPPwsln/qWzdbmaIvnhZ9v2P+CuecK+fpUfsV2mA==" + }, + "mime-types": { + "version": "2.1.24", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.24.tgz", + "integrity": "sha512-WaFHS3MCl5fapm3oLxU4eYDw77IQM2ACcxQ9RIxfaC3ooc6PFuBMGZZsYpvoXS5D5QTWPieo1jjLdAm3TBP3cQ==", + "requires": { + "mime-db": "1.40.0" + } + }, + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "minimist": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", + "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", + "dev": true, + "optional": true + }, + "mkdirp": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", + "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", + "dev": true, + "optional": true, + "requires": { + "minimist": "0.0.8" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + }, + "negotiator": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz", + "integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==" + }, + "nthen": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/nthen/-/nthen-0.1.8.tgz", + "integrity": "sha512-Oh2CwIbhj+wUT94lQV7LKmmgw3UYAGGd8oLIqp6btQN3Bz3PuWp4BuvtUo35H3rqDknjPfKx5P6mt7v+aJNjcw==" + }, + "on-finished": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=", + "requires": { + "ee-first": "1.1.1" + } + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "dev": true, + "requires": { + "wrappy": "1" + } + }, + "os-tmpdir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=", + "dev": true + }, + "pako": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.10.tgz", + "integrity": "sha512-0DTvPVU3ed8+HNXOu5Bs+o//Mbdj9VNQMUOe9oKCwh8l0GNwpTDMKCWbRjgtD291AWnkAgkqA/LOnQS8AmS1tw==", + "dev": true + }, + "parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==" + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", + "dev": true + }, + "path-to-regexp": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", + "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=" + }, + "path-type": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-3.0.0.tgz", + "integrity": "sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==", + "dev": true, + "requires": { + "pify": "^3.0.0" + } + }, + "pify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", + "dev": true + }, + "postcss": { + "version": "6.0.23", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-6.0.23.tgz", + "integrity": "sha512-soOk1h6J3VMTZtVeVpv15/Hpdl2cBLX3CAw4TAbkpTJiNPk9YP/zWcD1ND+xEtvyuuvKzbxliTOIyvkSeSJ6ag==", + "dev": true, + "requires": { + "chalk": "^2.4.1", + "source-map": "^0.6.1", + "supports-color": "^5.4.0" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } + } + }, + "postcss-less": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/postcss-less/-/postcss-less-1.1.5.tgz", + "integrity": "sha512-QQIiIqgEjNnquc0d4b6HDOSFZxbFQoy4MPpli2lSLpKhMyBkKwwca2HFqu4xzxlKID/F2fxSOowwtKpgczhF7A==", + "dev": true, + "requires": { + "postcss": "^5.2.16" + }, + "dependencies": { + "ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", + "dev": true + }, + "chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "dev": true, + "requires": { + "ansi-styles": "^2.2.1", + "escape-string-regexp": "^1.0.2", + "has-ansi": "^2.0.0", + "strip-ansi": "^3.0.0", + "supports-color": "^2.0.0" + }, + "dependencies": { + "supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", + "dev": true + } + } + }, + "has-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-1.0.0.tgz", + "integrity": "sha1-nZ55MWXOAXoA8AQYxD+UKnsdEfo=", + "dev": true + }, + "postcss": { + "version": "5.2.18", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-5.2.18.tgz", + "integrity": "sha512-zrUjRRe1bpXKsX1qAJNJjqZViErVuyEkMTRrwu4ud4sbTtIBRmtaYDrHmcGgmrbsW3MHfmtIf+vJumgQn+PrXg==", + "dev": true, + "requires": { + "chalk": "^1.1.3", + "js-base64": "^2.1.9", + "source-map": "^0.5.6", + "supports-color": "^3.2.3" + } + }, + "supports-color": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-3.2.3.tgz", + "integrity": "sha1-ZawFBLOVQXHYpklGsq48u4pfVPY=", + "dev": true, + "requires": { + "has-flag": "^1.0.0" + } + } + } + }, + "postcss-selector-parser": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-3.1.1.tgz", + "integrity": "sha1-T4dfSvsMllc9XPTXQBGu4lCn6GU=", + "dev": true, + "requires": { + "dot-prop": "^4.1.1", + "indexes-of": "^1.0.1", + "uniq": "^1.0.1" + } + }, + "postcss-values-parser": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/postcss-values-parser/-/postcss-values-parser-1.5.0.tgz", + "integrity": "sha512-3M3p+2gMp0AH3da530TlX8kiO1nxdTnc3C6vr8dMxRLIlh8UYkz0/wcwptSXjhtx2Fr0TySI7a+BHDQ8NL7LaQ==", + "dev": true, + "requires": { + "flatten": "^1.0.2", + "indexes-of": "^1.0.1", + "uniq": "^1.0.1" + } + }, + "process-nextick-args": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz", + "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==", + "dev": true + }, + "promise": { + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/promise/-/promise-7.3.1.tgz", + "integrity": "sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==", + "dev": true, + "optional": true, + "requires": { + "asap": "~2.0.3" + } + }, + "proxy-addr": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.5.tgz", + "integrity": "sha512-t/7RxHXPH6cJtP0pRG6smSr9QJidhB+3kXu0KgXnbGYMgzEnUxRQ4/LDdfOwZEMyIh3/xHb8PX3t+lfL9z+YVQ==", + "requires": { + "forwarded": "~0.1.2", + "ipaddr.js": "1.9.0" + } + }, + "prr": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz", + "integrity": "sha1-0/wRS6BplaRexok/SEzrHXj19HY=", + "dev": true, + "optional": true + }, + "pull-stream": { + "version": "3.6.12", + "resolved": "https://registry.npmjs.org/pull-stream/-/pull-stream-3.6.12.tgz", + "integrity": "sha512-+LO1XIVyTMmeoH26UHznpgrgX2npTVYccTkMpgk/EyiQjFt1FmoNm+w+/zMLuz9U3bpvT5sSUicMKEe/2JjgEA==" + }, + "qs": { + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", + "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==" + }, + "range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==" + }, + "raw-body": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.3.3.tgz", + "integrity": "sha512-9esiElv1BrZoI3rCDuOuKCBRbuApGGaDPQfjSflGxdy4oyzqghxu6klEkkVIvBje+FF0BX9coEv8KqW6X/7njw==", + "requires": { + "bytes": "3.0.0", + "http-errors": "1.6.3", + "iconv-lite": "0.4.23", + "unpipe": "1.0.0" + } + }, + "rcfinder": { + "version": "0.1.9", + "resolved": "https://registry.npmjs.org/rcfinder/-/rcfinder-0.1.9.tgz", + "integrity": "sha1-8+gPOH3fmugK4wpBADKWQuroERU=", + "dev": true, + "requires": { + "lodash.clonedeep": "^4.3.2" + } + }, + "readable-stream": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", + "integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=", + "dev": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.1", + "isarray": "0.0.1", + "string_decoder": "~0.10.x" + } + }, + "replify": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/replify/-/replify-1.2.0.tgz", + "integrity": "sha1-lAFm0gfRDphhT+SSU60vCsAZ9+E=" + }, + "rimraf": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", + "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", + "dev": true, + "requires": { + "glob": "^7.1.3" + } + }, + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "saferphore": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/saferphore/-/saferphore-0.0.1.tgz", + "integrity": "sha1-zJYu2k4rJFLmQ3/TLc+29p7y6mM=" + }, + "sax": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", + "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==", + "dev": true + }, + "selenium-webdriver": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/selenium-webdriver/-/selenium-webdriver-3.6.0.tgz", + "integrity": "sha512-WH7Aldse+2P5bbFBO4Gle/nuQOdVwpHMTL6raL3uuBj/vPG07k6uzt3aiahu352ONBr5xXh0hDlM3LhtXPOC4Q==", + "dev": true, + "requires": { + "jszip": "^3.1.3", + "rimraf": "^2.5.4", + "tmp": "0.0.30", + "xml2js": "^0.4.17" + } + }, + "send": { + "version": "0.16.2", + "resolved": "https://registry.npmjs.org/send/-/send-0.16.2.tgz", + "integrity": "sha512-E64YFPUssFHEFBvpbbjr44NCLtI1AohxQ8ZSiJjQLskAdKuriYEP6VyGEsRDH8ScozGpkaX1BGvhanqCwkcEZw==", + "requires": { + "debug": "2.6.9", + "depd": "~1.1.2", + "destroy": "~1.0.4", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "~1.6.2", + "mime": "1.4.1", + "ms": "2.0.0", + "on-finished": "~2.3.0", + "range-parser": "~1.2.0", + "statuses": "~1.4.0" + } + }, + "serve-static": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.13.2.tgz", + "integrity": "sha512-p/tdJrO4U387R9oMjb1oj7qSMaMfmOyd4j9hOFoxZe2baQszgHcSWjuya/CiT5kgZZKRudHNOA0pYXOl8rQ5nw==", + "requires": { + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "parseurl": "~1.3.2", + "send": "0.16.2" + } + }, + "set-immediate-shim": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/set-immediate-shim/-/set-immediate-shim-1.0.1.tgz", + "integrity": "sha1-SysbJ+uAip+NzEgaWOXlb1mfP2E=", + "dev": true + }, + "setprototypeof": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz", + "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==" + }, + "shelljs": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/shelljs/-/shelljs-0.3.0.tgz", + "integrity": "sha1-NZbmMHp4FUT1kfN9phg2DzHbV7E=", + "dev": true + }, + "slash": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-1.0.0.tgz", + "integrity": "sha1-xB8vbDn8FtHNF61LXYlhFK5HDVU=", + "dev": true + }, + "sortify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/sortify/-/sortify-1.0.4.tgz", + "integrity": "sha1-8BeGh8gyMb6KNPwOxUYuqVe2AoQ=", + "requires": { + "lex": "^1.7.9" + } + }, + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", + "dev": true + }, + "statuses": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.4.0.tgz", + "integrity": "sha512-zhSCtt8v2NDrRlPQpCNtw/heZLtfUDqxBM1udqikb/Hbk52LK4nQSwr10u77iopCW5LsyHpuXS0GnEc48mLeew==" + }, + "stream-to-pull-stream": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/stream-to-pull-stream/-/stream-to-pull-stream-1.7.3.tgz", + "integrity": "sha512-6sNyqJpr5dIOQdgNy/xcDWwDuzAsAwVzhzrWlAPAQ7Lkjx/rv0wgvxEyKwTq6FmNd5rjTrELt/CLmaSw7crMGg==", + "requires": { + "looper": "^3.0.0", + "pull-stream": "^3.2.3" + } + }, + "string_decoder": { + "version": "0.10.31", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=", + "dev": true + }, + "strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "dev": true, + "requires": { + "ansi-regex": "^2.0.0" + } + }, + "strip-json-comments": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-1.0.4.tgz", + "integrity": "sha1-HhX7ysl9Pumb8tc7TGVrCCu6+5E=", + "dev": true + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + }, + "tiny-each-async": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/tiny-each-async/-/tiny-each-async-2.0.3.tgz", + "integrity": "sha1-jru/1tYpXxNwAD+7NxYq/loKUdE=" + }, + "tmp": { + "version": "0.0.30", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.30.tgz", + "integrity": "sha1-ckGdSovn1s51FI/YsyTlk6cRwu0=", + "dev": true, + "requires": { + "os-tmpdir": "~1.0.1" + } + }, + "tweetnacl": { + "version": "0.12.2", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.12.2.tgz", + "integrity": "sha1-vVn4kFB4VvsKETasw6i0RUfinds=" + }, + "type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "requires": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + } + }, + "ultron": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ultron/-/ultron-1.1.1.tgz", + "integrity": "sha512-UIEXBNeYmKptWH6z8ZnqTeS8fV74zG0/eRU9VGkpzz+LIJNs8W/zM/L+7ctCkRrgbNnnR0xxw4bKOr0cW0N0Og==" + }, + "uniq": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/uniq/-/uniq-1.0.1.tgz", + "integrity": "sha1-sxxa6CVIRKOoKBVBzisEuGWnNP8=", + "dev": true + }, + "universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==" + }, + "unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=" + }, + "util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", + "dev": true + }, + "utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=" + }, + "vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=" + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", + "dev": true + }, + "ws": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-3.3.3.tgz", + "integrity": "sha512-nnWLa/NwZSt4KQJu51MYlCcSQ5g7INpOrOMt4XV8j4dqTXdmlUmSHQ8/oLC069ckre0fRsgfvsKwbTdtKLCDkA==", + "requires": { + "async-limiter": "~1.0.0", + "safe-buffer": "~5.1.0", + "ultron": "~1.1.0" + } + }, + "xml2js": { + "version": "0.4.19", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.19.tgz", + "integrity": "sha512-esZnJZJOiJR9wWKMyuvSE1y6Dq5LCuJanqhxslH2bxM6duahNZ+HMpCLhBQGZkbX6xRf8x1Y2eJlgt2q3qo49Q==", + "dev": true, + "requires": { + "sax": ">=0.6.0", + "xmlbuilder": "~9.0.1" + } + }, + "xmlbuilder": { + "version": "9.0.7", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-9.0.7.tgz", + "integrity": "sha1-Ey7mPS7FVlxVfiD0wi35rKaGsQ0=", + "dev": true + } + } +} diff --git a/package.json b/package.json index 0761c18f6..06c187f11 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,7 @@ "url": "git://github.com/xwiki-labs/cryptpad.git" }, "dependencies": { - "chainpad-server": "~3.0.0", + "chainpad-server": "~3.0.2", "express": "~4.16.0", "fs-extra": "^7.0.0", "nthen": "~0.1.0", @@ -18,12 +18,11 @@ "sortify": "^1.0.4", "stream-to-pull-stream": "^1.7.2", "tweetnacl": "~0.12.2", - "ws": "^1.0.1", + "ws": "^3.3.1", "get-folder-size": "^2.0.1" }, "devDependencies": { "flow-bin": "^0.59.0", - "heapdump": "^0.3.9", "jshint": "~2.9.1", "less": "2.7.1", "lesshint": "^4.5.0", diff --git a/readme.md b/readme.md index d12e2e376..b6a3d9b17 100644 --- a/readme.md +++ b/readme.md @@ -62,13 +62,13 @@ via our [GitHub issue tracker](https://github.com/xwiki-labs/cryptpad/issues/), [Matrix channel](https://riot.im/app/#/room/#cryptpad:matrix.org), or by [e-mail](mailto:research@xwiki.com). +# Team + +CryptPad is actively developed by a team at [XWiki SAS](https://www.xwiki.com), a company that has been building Open-Source software since 2004 with contributors from around the world. Between 2015 and 2019 it was funded by a research grant from the French state through [BPI France](https://www.bpifrance.fr/). It is currently financed by [NLnet PET](https://nlnet.nl/PET/), subscribers of CryptPad.fr and donations to our [Open-Collective campaign](https://opencollective.com/cryptpad). + # Contributing -We love Open Source and we love contribution. It is our intent to keep this project available -under the AGPL license forever but in order to finance more development on this and other FOSS -projects, we also wish to sell other licenses to this software. Before making a pull request, -please read and -[sign the Commons Management Agreement](https://www.clahub.com/agreements/cjdelisle/cryptpad). +We love Open Source and we love contribution. Learn more about [contributing](https://github.com/xwiki-labs/cryptpad/wiki/Contributor-overview). If you have any questions or comments, or if you're interested in contributing to Cryptpad, come say hi on IRC, `#cryptpad` on Freenode. diff --git a/rpc.js b/rpc.js index adc869432..a69033cc1 100644 --- a/rpc.js +++ b/rpc.js @@ -814,6 +814,7 @@ var clearOwnedChannel = function (Env, channelId, unsafeKey, cb) { return void cb('INSUFFICIENT_PERMISSIONS'); } + // FIXME COLDSTORAGE return void Env.msgStore.clearChannel(channelId, function (e) { cb(e); }); @@ -900,6 +901,20 @@ var removeOwnedChannel = function (Env, channelId, unsafeKey, cb) { if (metadata.owners.indexOf(unsafeKey) === -1) { return void cb('INSUFFICIENT_PERMISSIONS'); } + + // if the admin has configured data retention... + // temporarily archive the file instead of removing it + if (Env.retainData) { + return void Env.msgStore.archiveChannel(channelId, function (e) { + Log.info('ARCHIVAL_CHANNEL_BY_OWNER_RPC', { + unsafeKey: unsafeKey, + channelId: channelId, + status: e? String(e): 'SUCCESS', + }); + cb(e); + }); + } + return void Env.msgStore.removeChannel(channelId, function (e) { Log.info('DELETION_CHANNEL_BY_OWNER_RPC', { unsafeKey: unsafeKey, @@ -1430,6 +1445,7 @@ var removeLoginBlock = function (Env, msg, cb) { return void cb('E_INVALID_BLOCK_PATH'); } + // FIXME COLDSTORAGE Fs.unlink(path, function (err) { Log.info('DELETION_BLOCK_BY_OWNER_RPC', { publicKey: publicKey, @@ -1645,6 +1661,7 @@ RPC.create = function ( }; var Env = { + retainData: config.retainData || false, defaultStorageLimit: config.defaultStorageLimit, maxUploadSize: config.maxUploadSize || (20 * 1024 * 1024), Sessions: {}, diff --git a/scripts/delete-inactive.js b/scripts/delete-inactive.js index 9dbfb0707..1cbe799d4 100644 --- a/scripts/delete-inactive.js +++ b/scripts/delete-inactive.js @@ -7,6 +7,14 @@ const config = require("../lib/load-config"); if (!config.inactiveTime || typeof(config.inactiveTime) !== "number") { return; } +/* Instead of this script you should probably use + evict-inactive.js which moves things to an archive directory + in case the data that would have been deleted turns out to be important. + it also handles removing that archived data after a set period of time + + it only works for channels at the moment, though, and nothing else. +*/ + let inactiveTime = +new Date() - (config.inactiveTime * 24 * 3600 * 1000); let inactiveConfig = { unpinned: true, diff --git a/scripts/diagnose-archive-conflicts.js b/scripts/diagnose-archive-conflicts.js new file mode 100644 index 000000000..8617150fc --- /dev/null +++ b/scripts/diagnose-archive-conflicts.js @@ -0,0 +1,63 @@ +var nThen = require("nthen"); + +var Store = require("../storage/file"); +var config = require("../lib/load-config"); + +var store; +var Log; +nThen(function (w) { + // load the store which will be used for iterating over channels + // and performing operations like archival and deletion + Store.create(config, w(function (_) { + store = _; + })); + + // load the logging module so that you have a record of which + // files were archived or deleted at what time + var Logger = require("../lib/log"); + Logger.create(config, w(function (_) { + Log = _; + })); +}).nThen(function (w) { + // count the number of files which have been restored in this run + var conflicts = 0; + + var handler = function (err, item, cb) { + if (err) { + Log.error('DIAGNOSE_ARCHIVE_CONFLICTS_ITERATION', err); + return void cb(); + } + + // check if such a file exists on the server + store.isChannelAvailable(item.channel, function (err, available) { + // weird edge case? + if (err) { return void cb(); } + + // the channel doesn't exist in the database + if (!available) { return void cb(); } + + // the channel is available + // that means it's a duplicate of something in the archive + conflicts++; + Log.info('DIAGNOSE_ARCHIVE_CONFLICT_DETECTED', item.channel); + cb(); + }); + }; + + // if you hit an error, log it + // otherwise, when there are no more channels to process + // log some stats about how many were removed + var done = function (err) { + if (err) { + return Log.error('DIAGNOSE_ARCHIVE_CONFLICTS_FINAL_ERROR', err); + } + Log.info('DIAGNOSE_ARCHIVE_CONFLICTS_COUNT', conflicts); + }; + + store.listArchivedChannels(handler, w(done)); +}).nThen(function () { + // the store will keep this script running if you don't shut it down + store.shutdown(); + Log.shutdown(); +}); + diff --git a/scripts/evict-inactive.js b/scripts/evict-inactive.js new file mode 100644 index 000000000..ff9a3c343 --- /dev/null +++ b/scripts/evict-inactive.js @@ -0,0 +1,169 @@ +var nThen = require("nthen"); + +var Store = require("../storage/file"); +var Pinned = require("./pinned"); +var config = require("../lib/load-config"); + +// the administrator should have set an 'inactiveTime' in their config +// if they didn't, just exit. +if (!config.inactiveTime || typeof(config.inactiveTime) !== "number") { return; } + +// files which have not been changed since before this date can be considered inactive +var inactiveTime = +new Date() - (config.inactiveTime * 24 * 3600 * 1000); + +// files which were archived before this date can be considered safe to remove +var retentionTime = +new Date() - (config.archiveRetentionTime * 24 * 3600 * 1000); + +var store; +var pins; +var Log; +nThen(function (w) { + // load the store which will be used for iterating over channels + // and performing operations like archival and deletion + Store.create(config, w(function (_) { + store = _; + })); // load the list of pinned files so you know which files + // should not be archived or deleted + Pinned.load(w(function (err, _) { + if (err) { + w.abort(); + return void console.error(err); + } + pins = _; + }), { + pinPath: config.pinPath, + }); + + // load the logging module so that you have a record of which + // files were archived or deleted at what time + var Logger = require("../lib/log"); + Logger.create(config, w(function (_) { + Log = _; + })); +}).nThen(function (w) { + // this block will iterate over archived channels and remove them + // if they've been in cold storage for longer than your configured archive time + + // if the admin has not set an 'archiveRetentionTime', this block makes no sense + // so just skip it + if (typeof(config.archiveRetentionTime) !== "number") { return; } + + // count the number of files which have been removed in this run + var removed = 0; + + var handler = function (err, item, cb) { + if (err) { + Log.error('EVICT_ARCHIVED_CHANNEL_ITERATION', err); + return void cb(); + } + // don't mess with files that are freshly stored in cold storage + // based on ctime because that's changed when the file is moved... + if (+new Date(item.ctime) > retentionTime) { + return void cb(); + } + + // but if it's been stored for the configured time... + // expire it + store.removeArchivedChannel(item.channel, w(function (err) { + if (err) { + Log.error('EVICT_ARCHIVED_CHANNEL_REMOVAL_ERROR', { + error: err, + channel: item.channel, + }); + return void cb(); + } + Log.info('EVICT_ARCHIVED_CHANNEL_REMOVAL', item.channel); + removed++; + cb(); + })); + }; + + // if you hit an error, log it + // otherwise, when there are no more channels to process + // log some stats about how many were removed + var done = function (err) { + if (err) { + return Log.error('EVICT_ARCHIVED_FINAL_ERROR', err); + } + Log.info('EVICT_ARCHIVED_CHANNELS_REMOVED', removed); + }; + + store.listArchivedChannels(handler, w(done)); +}).nThen(function (w) { + var removed = 0; + var channels = 0; + var archived = 0; + + var handler = function (err, item, cb) { + channels++; + if (err) { + Log.error('EVICT_CHANNEL_ITERATION', err); + return void cb(); + } + // check if the database has any ephemeral channels + // if it does it's because of a bug, and they should be removed + if (item.channel.length === 34) { + return void store.removeChannel(item.channel, w(function (err) { + if (err) { + Log.error('EVICT_EPHEMERAL_CHANNEL_REMOVAL_ERROR', { + error: err, + channel: item.channel, + }); + return void cb(); + } + Log.info('EVICT_EPHEMERAL_CHANNEL_REMOVAL', item.channel); + cb(); + })); + } + + // bail out if the channel was modified recently + if (+new Date(item.mtime) > inactiveTime) { return void cb(); } + + // ignore the channel if it's pinned + if (pins[item.channel]) { return void cb(); } + + // if the server is configured to retain data, archive the channel + if (config.retainData) { + return void store.archiveChannel(item.channel, w(function (err) { + if (err) { + Log.error('EVICT_CHANNEL_ARCHIVAL_ERROR', { + error: err, + channel: item.channel, + }); + return void cb(); + } + Log.info('EVICT_CHANNEL_ARCHIVAL', item.channel); + archived++; + cb(); + })); + } + + // otherwise remove it + store.removeChannel(item.channel, w(function (err) { + if (err) { + Log.error('EVICT_CHANNEL_REMOVAL_ERROR', { + error: err, + channel: item.channel, + }); + return void cb(); + } + Log.info('EVICT_CHANNEL_REMOVAL', item.channel); + removed++; + cb(); + })); + }; + + var done = function () { + if (config.retainData) { + return void Log.info('EVICT_CHANNELS_ARCHIVED', archived); + } + return void Log.info('EVICT_CHANNELS_REMOVED', removed); + }; + + store.listChannels(handler, w(done)); +}).nThen(function () { + // the store will keep this script running if you don't shut it down + store.shutdown(); + Log.shutdown(); +}); + diff --git a/scripts/restore-archived.js b/scripts/restore-archived.js new file mode 100644 index 000000000..a420e35e5 --- /dev/null +++ b/scripts/restore-archived.js @@ -0,0 +1,61 @@ +var nThen = require("nthen"); + +var Store = require("../storage/file"); +var config = require("../lib/load-config"); + +var store; +var Log; +nThen(function (w) { + // load the store which will be used for iterating over channels + // and performing operations like archival and deletion + Store.create(config, w(function (_) { + store = _; + })); + + // load the logging module so that you have a record of which + // files were archived or deleted at what time + var Logger = require("../lib/log"); + Logger.create(config, w(function (_) { + Log = _; + })); +}).nThen(function (w) { + // count the number of files which have been restored in this run + var restored = 0; + + var handler = function (err, item, cb) { + if (err) { + Log.error('RESTORE_ARCHIVED_CHANNEL_ITERATION', err); + return void cb(); + } + + store.restoreArchivedChannel(item.channel, w(function (err) { + if (err) { + Log.error('RESTORE_ARCHIVED_CHANNEL_RESTORATION_ERROR', { + error: err, + channel: item.channel, + }); + return void cb(); + } + Log.info('RESTORE_ARCHIVED_CHANNEL_RESTORATION', item.channel); + restored++; + cb(); + })); + }; + + // if you hit an error, log it + // otherwise, when there are no more channels to process + // log some stats about how many were removed + var done = function (err) { + if (err) { + return Log.error('RESTORE_ARCHIVED_FINAL_ERROR', err); + } + Log.info('RESTORE_ARCHIVED_CHANNELS_RESTORED', restored); + }; + + store.listArchivedChannels(handler, w(done)); +}).nThen(function () { + // the store will keep this script running if you don't shut it down + store.shutdown(); + Log.shutdown(); +}); + diff --git a/storage/file.js b/storage/file.js index ba092a14e..99272fc6f 100644 --- a/storage/file.js +++ b/storage/file.js @@ -5,6 +5,7 @@ var Fs = require("fs"); var Fse = require("fs-extra"); var Path = require("path"); var nThen = require("nthen"); +var Semaphore = require("saferphore"); const ToPull = require('stream-to-pull-stream'); const Pull = require('pull-stream'); @@ -14,10 +15,18 @@ const isValidChannelId = function (id) { /^[a-zA-Z0-9=+-]*$/.test(id); }; +// 511 -> octal 777 +// read, write, execute permissions flag +const PERMISSIVE = 511; + var mkPath = function (env, channelId) { return Path.join(env.root, channelId.slice(0, 2), channelId) + '.ndjson'; }; +var mkArchivePath = function (env, channelId) { + return Path.join(env.archiveRoot, 'datastore', channelId.slice(0, 2), channelId) + '.ndjson'; +}; + var getMetadataAtPath = function (Env, path, cb) { var remainder = ''; var stream = Fs.createReadStream(path, { encoding: 'utf8' }); @@ -68,7 +77,7 @@ var closeChannel = function (env, channelName, cb) { } }; -var clearChannel = function (env, channelId, cb) { // FIXME deletion +var clearChannel = function (env, channelId, cb) { var path = mkPath(env, channelId); getMetadataAtPath(env, path, function (e, metadata) { if (e) { return cb(new Error(e)); } @@ -189,8 +198,7 @@ var checkPath = function (path, callback) { callback(err); return; } - // 511 -> octal 777 - Fse.mkdirp(Path.dirname(path), 511, function (err) { + Fse.mkdirp(Path.dirname(path), PERMISSIVE, function (err) { if (err && err.code !== 'EEXIST') { callback(err); return; @@ -200,11 +208,128 @@ var checkPath = function (path, callback) { }); }; -var removeChannel = function (env, channelName, cb) { // FIXME deletion +var removeChannel = function (env, channelName, cb) { var filename = mkPath(env, channelName); Fs.unlink(filename, cb); }; +// pass in the path so we can reuse the same function for archived files +var channelExists = function (filepath, channelName, cb) { + Fs.stat(filepath, function (err, stat) { + if (err) { + if (err.code === 'ENOENT') { + // no, the file doesn't exist + return void cb(void 0, false); + } + return void cb(err); + } + if (!stat.isFile()) { return void cb("E_NOT_FILE"); } + return void cb(void 0, true); + }); +}; + +var removeArchivedChannel = function (env, channelName, cb) { + var filename = mkArchivePath(env, channelName); + Fs.unlink(filename, cb); +}; + +var listChannels = function (root, handler, cb) { + // do twenty things at a time + var sema = Semaphore.create(20); + + var dirList = []; + + nThen(function (w) { + // the root of your datastore contains nested directories... + Fs.readdir(root, w(function (err, list) { + if (err) { + w.abort(); + // TODO check if we normally return strings or errors + return void cb(err); + } + dirList = list; + })); + }).nThen(function (w) { + // search inside the nested directories + // stream it so you don't put unnecessary data in memory + var wait = w(); + dirList.forEach(function (dir) { + sema.take(function (give) { + var nestedDirPath = Path.join(root, dir); + Fs.readdir(nestedDirPath, w(give(function (err, list) { + if (err) { return void handler(err); } // Is this correct? + + list.forEach(function (item) { + // ignore things that don't match the naming pattern + if (/^\./.test(item) || !/[0-9a-fA-F]{32,}\.ndjson$/.test(item)) { return; } + var filepath = Path.join(nestedDirPath, item); + var channel = filepath.replace(/\.ndjson$/, '').replace(/.*\//, ''); + if ([32, 34].indexOf(channel.length) === -1) { return; } + + // otherwise throw it on the pile + sema.take(function (give) { + var next = w(give()); + Fs.stat(filepath, w(function (err, stats) { + if (err) { + return void handler(err); + } + + handler(void 0, { + channel: channel, + atime: stats.atime, + mtime: stats.mtime, + ctime: stats.ctime, + size: stats.size, + }, next); + })); + }); + }); + }))); + }); + }); + wait(); + }).nThen(function () { + cb(); + }); +}; + +// move a channel's log file from its current location +// to an equivalent location in the cold storage directory +var archiveChannel = function (env, channelName, cb) { + if (!env.retainData) { + return void cb("ARCHIVES_DISABLED"); + } + + // ctime is the most reliable indicator of when a file was archived + // because it is used to indicate changes to the files metadata + // and not its contents + // if we find that this is not reliable in production, we can update it manually + // https://nodejs.org/api/fs.html#fs_fs_utimes_path_atime_mtime_callback + + // check what the channel's path should be (in its current location) + var currentPath = mkPath(env, channelName); + + // construct a parallel path in the new location + var archivePath = mkArchivePath(env, channelName); + + // use Fse.move to move it, Fse makes paths to the directory when you use it. + // https://github.com/jprichardson/node-fs-extra/blob/HEAD/docs/move.md + Fse.move(currentPath, archivePath, { overwrite: true }, cb); +}; + +var unarchiveChannel = function (env, channelName, cb) { + // very much like 'archiveChannel' but in the opposite direction + + // the file is currently archived + var currentPath = mkArchivePath(env, channelName); + var unarchivedPath = mkPath(env, channelName); + + // if a file exists in the unarchived path, you probably don't want to clobber its data + // so unlike 'archiveChannel' we won't overwrite. + // Fse.move will call back with EEXIST in such a situation + Fse.move(currentPath, unarchivedPath, cb); +}; + var flushUnusedChannels = function (env, cb, frame) { var currentTime = +new Date(); @@ -413,19 +538,30 @@ module.exports.create = function ( ) { var env = { root: conf.filePath || './datastore', + archiveRoot: conf.archivePath || './data/archive', + retainData: conf.retainData, channels: { }, channelExpirationMs: conf.channelExpirationMs || 30000, verbose: conf.verbose, openFiles: 0, openFileLimit: conf.openFileLimit || 2048, }; - // 0x1ff -> 777 var it; - Fse.mkdirp(env.root, 0x1ff, function (err) { - if (err && err.code !== 'EEXIST') { - // TODO: somehow return a nice error - throw err; - } + + nThen(function (w) { + // make sure the store's directory exists + Fse.mkdirp(env.root, PERMISSIVE, w(function (err) { + if (err && err.code !== 'EEXIST') { + throw err; + } + })); + // make sure the cold storage directory exists + Fse.mkdirp(env.archiveRoot, PERMISSIVE, w(function (err) { + if (err && err.code !== 'EEXIST') { + throw err; + } + })); + }).nThen(function () { cb({ readMessagesBin: (channelName, start, asyncMsgHandler, cb) => { if (!isValidChannelId(channelName)) { return void cb(new Error('EINVAL')); } @@ -449,6 +585,10 @@ module.exports.create = function ( cb(err); }); }, + removeArchivedChannel: function (channelName, cb) { + if (!isValidChannelId(channelName)) { return void cb(new Error('EINVAL')); } + removeArchivedChannel(env, channelName, cb); + }, closeChannel: function (channelName, cb) { if (!isValidChannelId(channelName)) { return void cb(new Error('EINVAL')); } closeChannel(env, channelName, cb); @@ -468,6 +608,32 @@ module.exports.create = function ( if (!isValidChannelId(channelName)) { return void cb(new Error('EINVAL')); } clearChannel(env, channelName, cb); }, + listChannels: function (handler, cb) { + listChannels(env.root, handler, cb); + }, + isChannelAvailable: function (channelName, cb) { + if (!isValidChannelId(channelName)) { return void cb(new Error('EINVAL')); } + // construct the path + var filepath = mkPath(env, channelName); + channelExists(filepath, channelName, cb); + }, + isChannelArchived: function (channelName, cb) { + if (!isValidChannelId(channelName)) { return void cb(new Error('EINVAL')); } + // construct the path + var filepath = mkArchivePath(env, channelName); + channelExists(filepath, channelName, cb); + }, + listArchivedChannels: function (handler, cb) { + listChannels(Path.join(env.archiveRoot, 'datastore'), handler, cb); + }, + archiveChannel: function (channelName, cb) { + if (!isValidChannelId(channelName)) { return void cb(new Error('EINVAL')); } + archiveChannel(env, channelName, cb); + }, + restoreArchivedChannel: function (channelName, cb) { + if (!isValidChannelId(channelName)) { return void cb(new Error('EINVAL')); } + unarchiveChannel(env, channelName, cb); + }, log: function (channelName, content, cb) { message(env, channelName, content, cb); }, diff --git a/storage/tasks.js b/storage/tasks.js index 0e09d0ad0..22f89d7e2 100644 --- a/storage/tasks.js +++ b/storage/tasks.js @@ -83,6 +83,7 @@ var write = function (env, task, cb) { }; var remove = function (env, path, cb) { + // FIXME COLDSTORAGE? Fs.unlink(path, cb); }; diff --git a/www/admin/index.html b/www/admin/index.html index e3f7eacc4..79a96c97b 100644 --- a/www/admin/index.html +++ b/www/admin/index.html @@ -6,33 +6,7 @@ - +