Compare commits
257 commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
e0a3d2bf2c | ||
![]() |
b93256316b | ||
![]() |
8a82e53e82 | ||
![]() |
6215bb1555 | ||
![]() |
d7e929a0d1 | ||
![]() |
154a8a90c1 | ||
![]() |
0a6bf39353 | ||
![]() |
c0cf7bbda2 | ||
![]() |
197b0bcf59 | ||
![]() |
56a0e830bd | ||
![]() |
b3317df707 | ||
![]() |
9931074905 | ||
![]() |
0088a4ccc0 | ||
![]() |
11aad6eac5 | ||
![]() |
1fb2f45285 | ||
![]() |
d0f21e8078 | ||
![]() |
c71100c82a | ||
![]() |
4f2dd96708 | ||
![]() |
d8456c2c51 | ||
![]() |
b7324f3a5b | ||
![]() |
6890165f67 | ||
![]() |
5124572dba | ||
![]() |
0c1819bb15 | ||
![]() |
906990991e | ||
![]() |
48bdf734c5 | ||
![]() |
cf63e0e804 | ||
![]() |
ec66c2dc4e | ||
![]() |
67dfc94ef3 | ||
![]() |
f54f3ccaa2 | ||
![]() |
a35e2e58a3 | ||
![]() |
6ad2885a16 | ||
![]() |
70662888b1 | ||
![]() |
9f09a79986 | ||
![]() |
db64c0467a | ||
![]() |
ca52f84aa5 | ||
![]() |
6fff664947 | ||
![]() |
439ac0ab7c | ||
![]() |
dee6b3e9cc | ||
![]() |
dc9d072472 | ||
![]() |
91773832c1 | ||
![]() |
65730db0db | ||
![]() |
a86221b1cc | ||
![]() |
bcc53f73c6 | ||
![]() |
b0444f488b | ||
![]() |
70a11e5300 | ||
![]() |
f62a99882d | ||
![]() |
309c7d63ac | ||
![]() |
1d75366f66 | ||
![]() |
0a849fb7c6 | ||
![]() |
88725df09d | ||
![]() |
5a92e7e5e7 | ||
![]() |
71541fc2b6 | ||
![]() |
c524804c63 | ||
![]() |
5b4c0d2540 | ||
![]() |
e7f3c91d0b | ||
![]() |
8bb198b73e | ||
![]() |
9e188bc76c | ||
![]() |
1353a54c49 | ||
![]() |
4ae007167d | ||
![]() |
660f36e584 | ||
![]() |
3dede083cd | ||
![]() |
26e81455ff | ||
![]() |
4ceac20623 | ||
![]() |
073accfe65 | ||
![]() |
6306a433e8 | ||
![]() |
1da317bcc1 | ||
![]() |
08f597405c | ||
![]() |
c624766edc | ||
![]() |
e030c46a9c | ||
![]() |
d081affa38 | ||
![]() |
71372fcbc1 | ||
![]() |
671390ca24 | ||
![]() |
9221b86660 | ||
![]() |
fd2e954b3e | ||
![]() |
c528ad3147 | ||
![]() |
df9c7ea734 | ||
![]() |
e32ea7d0aa | ||
![]() |
55ad08fd96 | ||
![]() |
96d53e4118 | ||
![]() |
bce861bcaf | ||
![]() |
643287e235 | ||
![]() |
c619be58ae | ||
![]() |
9b8b11ffc3 | ||
![]() |
1725ff434e | ||
![]() |
e1d6224570 | ||
![]() |
38746b86fd | ||
![]() |
64644b57e3 | ||
![]() |
625fdf5bca | ||
![]() |
951c613095 | ||
![]() |
16e78847a2 | ||
![]() |
310271c10f | ||
![]() |
55344f8a9d | ||
![]() |
2b22e8cd05 | ||
![]() |
47ff32fc9f | ||
![]() |
b598a1c090 | ||
![]() |
3ae9e6adeb | ||
![]() |
33e7e0f5ba | ||
![]() |
ca3b5cf7ca | ||
![]() |
44a25e4156 | ||
![]() |
000854104f | ||
![]() |
1a0ddf9a05 | ||
![]() |
0ac1eeed2c | ||
![]() |
2457545502 | ||
![]() |
75637807eb | ||
![]() |
3b0ef1c0d6 | ||
![]() |
22c9560855 | ||
![]() |
dda964ebf9 | ||
![]() |
560747106b | ||
![]() |
81741dcc76 | ||
![]() |
58a0800384 | ||
![]() |
7323f584c1 | ||
![]() |
c616412449 | ||
![]() |
b9faece68e | ||
![]() |
097bdf8853 | ||
![]() |
e96e09f664 | ||
![]() |
0ebea72b3e | ||
![]() |
9cac202892 | ||
![]() |
b41c8087f9 | ||
![]() |
9fcc8e36b0 | ||
![]() |
905ca545c7 | ||
![]() |
742b5de7e1 | ||
![]() |
5d7162c4a3 | ||
![]() |
4c5d8e3bd3 | ||
![]() |
709997ba72 | ||
![]() |
b880516edf | ||
![]() |
fa716a7da6 | ||
![]() |
40fc5c35ca | ||
![]() |
8d72c81150 | ||
![]() |
383b84ef1f | ||
![]() |
7cba51b729 | ||
![]() |
7d024a9998 | ||
![]() |
eabff183ec | ||
![]() |
d04829cbf2 | ||
![]() |
af52b96ab4 | ||
![]() |
ec26a8e38f | ||
![]() |
41f5615acc | ||
![]() |
756d1b1d14 | ||
![]() |
a37a0c8678 | ||
![]() |
8c50da564f | ||
![]() |
214f723232 | ||
![]() |
f2a6af409e | ||
![]() |
b346e3e3ae | ||
![]() |
aea428372d | ||
![]() |
ea8efb9d93 | ||
![]() |
2b7164f589 | ||
![]() |
08399059e9 | ||
![]() |
aea40fa11b | ||
![]() |
e51c753c0d | ||
![]() |
6520d71faa | ||
![]() |
97b0b10863 | ||
![]() |
50e76e0895 | ||
![]() |
610e036e26 | ||
![]() |
e695a8b481 | ||
![]() |
d33ddf643b | ||
![]() |
512bd32368 | ||
![]() |
3c42de0efd | ||
![]() |
b7582230cf | ||
![]() |
592ff3fb4a | ||
![]() |
927203cb96 | ||
![]() |
48237807fa | ||
![]() |
38a4552d52 | ||
![]() |
9d2d81e063 | ||
![]() |
81d6c90c4e | ||
![]() |
2bdaa0c4bd | ||
![]() |
00d3bebc27 | ||
![]() |
ea384ff5d3 | ||
![]() |
062c439ec0 | ||
![]() |
54e528980b | ||
![]() |
b9292abefe | ||
![]() |
1520942ac9 | ||
![]() |
0e17cd567c | ||
![]() |
7b21b199c2 | ||
![]() |
6214b07a30 | ||
![]() |
941d87976b | ||
![]() |
8db3fed6fb | ||
![]() |
78ca1f06e0 | ||
![]() |
9fdc079878 | ||
![]() |
f08d078236 | ||
![]() |
46ae4a220b | ||
![]() |
d0932c26ea | ||
![]() |
798c399a38 | ||
![]() |
825e3942a2 | ||
![]() |
755459f57e | ||
![]() |
42f5ca9701 | ||
![]() |
1a923d21b5 | ||
![]() |
3bd9f00c25 | ||
![]() |
fa1c64369f | ||
![]() |
9280f47afc | ||
![]() |
3707b90d09 | ||
![]() |
21392f1157 | ||
![]() |
680d3ed948 | ||
![]() |
dee1e84e58 | ||
![]() |
214191e743 | ||
![]() |
84da34169d | ||
![]() |
93e1d2f41a | ||
![]() |
43e1845d28 | ||
![]() |
c01d6b73ea | ||
![]() |
a394fd995e | ||
![]() |
175712cfbd | ||
![]() |
e5d7378fd9 | ||
![]() |
20cf722b54 | ||
![]() |
1d6872e279 | ||
![]() |
a1ca355771 | ||
![]() |
dc816d0e59 | ||
![]() |
d6ac469e1a | ||
![]() |
62cfecd618 | ||
![]() |
9152d22913 | ||
![]() |
21b198fbd5 | ||
![]() |
0ffc960523 | ||
![]() |
77ea05a233 | ||
![]() |
a6162f7142 | ||
![]() |
4a6a3dfc36 | ||
![]() |
1e7efe3d98 | ||
![]() |
46381fd516 | ||
![]() |
1fe74f2be0 | ||
![]() |
35da83bf2a | ||
![]() |
bcfb9c5d09 | ||
![]() |
4df2578bb1 | ||
![]() |
e4f2955eae | ||
![]() |
72377d3438 | ||
![]() |
512c9803bd | ||
![]() |
4c45d6217d | ||
![]() |
b4b8060a78 | ||
![]() |
ed042b8515 | ||
![]() |
06bc58c93c | ||
![]() |
b58caed44f | ||
![]() |
174ade1c2e | ||
![]() |
31ce8c048b | ||
![]() |
ce401881d7 | ||
![]() |
c49e8e1062 | ||
![]() |
15648157c9 | ||
![]() |
4280edd5af | ||
![]() |
a3d4e2c502 | ||
![]() |
bed5443685 | ||
![]() |
f9f5d77cd0 | ||
![]() |
0f8a6a107a | ||
![]() |
02e8cb264f | ||
![]() |
385ac595b9 | ||
![]() |
6df0876286 | ||
![]() |
827a35f73e | ||
![]() |
eb3a9e8c89 | ||
![]() |
6c3ac403f6 | ||
![]() |
1ce2a60dd5 | ||
![]() |
f5bb74e921 | ||
![]() |
352fba6302 | ||
![]() |
ace2aa5d73 | ||
![]() |
3256b01276 | ||
![]() |
96244132c6 | ||
![]() |
a9cdd13543 | ||
![]() |
1b6c5b8f97 | ||
![]() |
27e6606516 | ||
![]() |
4902d304b6 | ||
![]() |
a182ff2dd1 | ||
![]() |
0361e3ce1c | ||
![]() |
32539e58ac | ||
![]() |
eeb1359d90 | ||
![]() |
e2dde364eb |
4
.gitattributes
vendored
|
@ -1,2 +1,2 @@
|
|||
public/locales/* linguist-documentation
|
||||
docs/* linguist-documentation
|
||||
public/locales/*/*.ftl linguist-documentation
|
||||
docs/** linguist-documentation
|
||||
|
|
1
.gitignore
vendored
|
@ -1,6 +1,7 @@
|
|||
node_modules
|
||||
coverage
|
||||
dist
|
||||
.env
|
||||
.idea
|
||||
.DS_Store
|
||||
.nyc_output
|
||||
|
|
137
.gitlab-ci.yml
|
@ -1,105 +1,72 @@
|
|||
image: "node:15-slim"
|
||||
|
||||
stages:
|
||||
- test
|
||||
- artifact
|
||||
- release
|
||||
|
||||
before_script:
|
||||
# Install dependencies
|
||||
- apt-get update
|
||||
- apt-get install -y git python3 build-essential libxtst6
|
||||
|
||||
# Prepare Chrome for puppeteer
|
||||
- apt-get install -y wget gnupg
|
||||
- wget -q -O - https://dl-ssl.google.com/linux/linux_signing_key.pub | apt-key add -
|
||||
- sh -c 'echo "deb [arch=amd64] http://dl.google.com/linux/chrome/deb/ stable main" >> /etc/apt/sources.list.d/google.list'
|
||||
- apt-get update
|
||||
- apt-get install -y google-chrome-stable fonts-ipafont-gothic fonts-wqy-zenhei fonts-thai-tlwg fonts-kacst fonts-freefont-ttf libxss1 --no-install-recommends
|
||||
|
||||
# Build Send, run npm tests
|
||||
test:
|
||||
stage: test
|
||||
image: "node:16-slim"
|
||||
only:
|
||||
- api
|
||||
- branches
|
||||
- chat
|
||||
- merge_requests
|
||||
- pushes
|
||||
- schedules
|
||||
- tags
|
||||
- triggers
|
||||
- web
|
||||
before_script:
|
||||
# Install dependencies
|
||||
- apt-get update
|
||||
- apt-get install -y git python3 build-essential libxtst6
|
||||
|
||||
# Prepare Chrome for puppeteer
|
||||
- apt-get install -y wget gnupg
|
||||
- wget -q -O - https://dl-ssl.google.com/linux/linux_signing_key.pub | apt-key add -
|
||||
- sh -c 'echo "deb [arch=amd64] http://dl.google.com/linux/chrome/deb/ stable main" >> /etc/apt/sources.list.d/google.list'
|
||||
- apt-get update
|
||||
- apt-get install -y gconf-service libasound2 libatk1.0-0 libc6 libcairo2 libcups2 libdbus-1-3 libexpat1 libfontconfig1 libgcc1 libgconf-2-4 libgdk-pixbuf2.0-0 libglib2.0-0 libgtk-3-0 libnspr4 libpango-1.0-0 libpangocairo-1.0-0 libstdc++6 libx11-6 libx11-xcb1 libxcb1 libxcomposite1 libxcursor1 libxdamage1 libxext6 libxfixes3 libxi6 libxrandr2 libxrender1 libxss1 libxtst6 ca-certificates fonts-liberation libappindicator1 libnss3 lsb-release xdg-utils
|
||||
- apt-get install -y google-chrome-stable fonts-ipafont-gothic fonts-wqy-zenhei fonts-thai-tlwg fonts-kacst fonts-freefont-ttf libxss1 --no-install-recommends
|
||||
script:
|
||||
- npm ci
|
||||
- npm run lint
|
||||
- npm test
|
||||
|
||||
# Build Docker image, export Docker image artifact
|
||||
artifact-docker:
|
||||
stage: artifact
|
||||
image: docker:latest
|
||||
needs: []
|
||||
services:
|
||||
- docker:dind
|
||||
variables:
|
||||
IMG_FILE: "send:git-$CI_COMMIT_SHORT_SHA.tar"
|
||||
IMG_NAME: "send:git-$CI_COMMIT_SHORT_SHA"
|
||||
before_script: []
|
||||
script:
|
||||
- docker build -t $IMG_NAME .
|
||||
- docker image save -o $IMG_FILE $IMG_NAME
|
||||
artifacts:
|
||||
name: artifact-docker
|
||||
paths:
|
||||
- $IMG_FILE
|
||||
expire_in: 1 week
|
||||
|
||||
# Release public Docker image for the master branch
|
||||
release-docker-master:
|
||||
stage: release
|
||||
image: docker:latest
|
||||
dependencies:
|
||||
- artifact-docker
|
||||
services:
|
||||
- docker:dind
|
||||
only:
|
||||
- master
|
||||
variables:
|
||||
IMG_IMPORT_FILE: "send:git-$CI_COMMIT_SHORT_SHA.tar"
|
||||
IMG_IMPORT_NAME: "send:git-$CI_COMMIT_SHORT_SHA"
|
||||
IMG_NAME: "registry.gitlab.com/timvisee/send:master-$CI_COMMIT_SHORT_SHA"
|
||||
before_script: []
|
||||
script:
|
||||
# Login in to registry
|
||||
- 'docker login registry.gitlab.com -u $DOCKER_USER -p $DOCKER_PASS'
|
||||
|
||||
# Load existing, retag for new image images
|
||||
- docker image load -i $IMG_IMPORT_FILE
|
||||
- docker tag $IMG_IMPORT_NAME $IMG_NAME
|
||||
|
||||
# Publish tagged image
|
||||
- docker push $IMG_NAME
|
||||
|
||||
- 'echo "Docker image artifact published, available as:" && echo " docker pull $IMG_NAME"'
|
||||
|
||||
# Release public Docker image for a version tag
|
||||
release-docker:
|
||||
stage: release
|
||||
image: docker:latest
|
||||
dependencies:
|
||||
- artifact-docker
|
||||
services:
|
||||
- docker:dind
|
||||
only:
|
||||
- /^v(\d+\.)*\d+$/
|
||||
variables:
|
||||
IMG_IMPORT_FILE: "send:git-$CI_COMMIT_SHORT_SHA.tar"
|
||||
IMG_IMPORT_NAME: "send:git-$CI_COMMIT_SHORT_SHA"
|
||||
IMG_NAME: "registry.gitlab.com/timvisee/send:$CI_COMMIT_REF_NAME"
|
||||
IMG_NAME_LATEST: "registry.gitlab.com/timvisee/send:latest"
|
||||
before_script: []
|
||||
- api
|
||||
- branches
|
||||
- chat
|
||||
- merge_requests
|
||||
- pushes
|
||||
- schedules
|
||||
- tags
|
||||
- triggers
|
||||
- web
|
||||
script:
|
||||
# Login in to registry
|
||||
- 'docker login registry.gitlab.com -u $DOCKER_USER -p $DOCKER_PASS'
|
||||
|
||||
# Load existing, retag for new image images
|
||||
- docker image load -i $IMG_IMPORT_FILE
|
||||
- docker tag $IMG_IMPORT_NAME $IMG_NAME
|
||||
- docker tag $IMG_IMPORT_NAME $IMG_NAME_LATEST
|
||||
|
||||
# Publish tagged image
|
||||
- docker push $IMG_NAME
|
||||
- docker push $IMG_NAME_LATEST
|
||||
|
||||
- 'echo "Docker image artifact published, available as:" && echo " docker pull $IMG_NAME_LATEST" && echo " docker pull $IMG_NAME"'
|
||||
- docker login "$CI_REGISTRY" -u "$CI_REGISTRY_USER" -p "$CI_REGISTRY_PASSWORD"
|
||||
- docker buildx create --name sendBuilder
|
||||
- docker buildx use sendBuilder
|
||||
- |
|
||||
if [ "$CI_PIPELINE_SOURCE" == "merge_request_event" ]; then
|
||||
IMAGE_NAMES="$CI_REGISTRY_IMAGE/mr:$CI_MERGE_REQUEST_IID"
|
||||
elif [ "$CI_COMMIT_TAG" != "" ]; then
|
||||
IMAGE_NAMES="$CI_REGISTRY_IMAGE:$CI_COMMIT_TAG $CI_REGISTRY_IMAGE:latest"
|
||||
else
|
||||
IMAGE_NAMES="$CI_REGISTRY_IMAGE/$CI_COMMIT_BRANCH:$CI_COMMIT_SHORT_SHA"
|
||||
fi
|
||||
- |
|
||||
for image in $IMAGE_NAMES; do
|
||||
docker buildx build --platform linux/amd64,linux/arm64 -t $image . --push
|
||||
done
|
||||
- |
|
||||
echo "Container image pushed. You can pull it with";
|
||||
for image in $IMAGE_NAMES; do
|
||||
echo "docker pull $image"
|
||||
done
|
||||
|
|
|
@ -11,3 +11,5 @@ rules:
|
|||
selector-list-comma-newline-after: null
|
||||
value-list-comma-newline-after: null
|
||||
at-rule-no-unknown: null
|
||||
# Conflicts with prettier
|
||||
string-quotes: null
|
||||
|
|
|
@ -49,6 +49,7 @@ Cynthia Pereira
|
|||
Daniel Thorn
|
||||
Daniela Arcese
|
||||
Danny Coates
|
||||
David Dumas
|
||||
Davide
|
||||
Derek Tamsen
|
||||
Dhyey Thakore
|
||||
|
|
32
Dockerfile
|
@ -4,41 +4,57 @@
|
|||
# License https://gitlab.com/timvisee/send/blob/master/LICENSE
|
||||
##
|
||||
|
||||
|
||||
# Build project
|
||||
FROM node:15.5.1-alpine AS builder
|
||||
FROM node:16.13-alpine3.13 AS builder
|
||||
|
||||
RUN set -x \
|
||||
# Change node uid/gid
|
||||
&& apk --no-cache add shadow \
|
||||
&& groupmod -g 1001 node \
|
||||
&& usermod -u 1001 -g 1001 node
|
||||
|
||||
RUN set -x \
|
||||
# Add user
|
||||
&& addgroup --gid 10001 app \
|
||||
&& addgroup --gid 1000 app \
|
||||
&& adduser --disabled-password \
|
||||
--gecos '' \
|
||||
--ingroup app \
|
||||
--home /app \
|
||||
--uid 10001 \
|
||||
--uid 1000 \
|
||||
app
|
||||
|
||||
COPY --chown=app:app . /app
|
||||
|
||||
USER app
|
||||
WORKDIR /app
|
||||
|
||||
RUN set -x \
|
||||
# Build
|
||||
&& PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true npm ci \
|
||||
&& npm run build
|
||||
|
||||
|
||||
# Main image
|
||||
FROM node:15.5.1-alpine
|
||||
FROM node:16.13-alpine3.13
|
||||
|
||||
RUN set -x \
|
||||
# Change node uid/gid
|
||||
&& apk --no-cache add shadow \
|
||||
&& groupmod -g 1001 node \
|
||||
&& usermod -u 1001 -g 1001 node
|
||||
|
||||
RUN set -x \
|
||||
# Add user
|
||||
&& addgroup --gid 10001 app \
|
||||
&& addgroup --gid 1000 app \
|
||||
&& adduser --disabled-password \
|
||||
--gecos '' \
|
||||
--ingroup app \
|
||||
--home /app \
|
||||
--uid 10001 \
|
||||
--uid 1000 \
|
||||
app
|
||||
|
||||
USER app
|
||||
WORKDIR /app
|
||||
|
||||
COPY --chown=app:app package*.json ./
|
||||
COPY --chown=app:app app app
|
||||
COPY --chown=app:app common common
|
||||
|
|
13
README.md
|
@ -1,4 +1,4 @@
|
|||
# [](https://gitlab.com/timvisee/send/) Send
|
||||
# [](https://gitlab.com/timvisee/send/) Send
|
||||
|
||||
[![Build status on GitLab CI][gitlab-ci-master-badge]][gitlab-ci-link]
|
||||
[![Latest release][release-badge]][release-link]
|
||||
|
@ -81,7 +81,7 @@ A file sharing experiment which allows you to send encrypted files to other user
|
|||
|
||||
## Requirements
|
||||
|
||||
- [Node.js 12.x](https://nodejs.org/)
|
||||
- [Node.js 16.x](https://nodejs.org/)
|
||||
- [Redis server](https://redis.io/) (optional for development)
|
||||
- [AWS S3](https://aws.amazon.com/s3/) or compatible service (optional)
|
||||
|
||||
|
@ -121,7 +121,7 @@ The server is configured with environment variables. See [server/config.js](serv
|
|||
|
||||
## Localization
|
||||
|
||||
see [docs/localization.md](docs/localization.md)
|
||||
See: [docs/localization.md](docs/localization.md)
|
||||
|
||||
---
|
||||
|
||||
|
@ -139,7 +139,11 @@ Find a list of public instances here: https://github.com/timvisee/send-instances
|
|||
|
||||
## Deployment
|
||||
|
||||
See also [docs/deployment.md](docs/deployment.md)
|
||||
See: [docs/deployment.md](docs/deployment.md)
|
||||
|
||||
Docker quickstart: [docs/docker.md](docs/docker.md)
|
||||
|
||||
AWS example using Ubuntu Server `20.04`: [docs/AWS.md](docs/AWS.md)
|
||||
|
||||
---
|
||||
|
||||
|
@ -148,6 +152,7 @@ See also [docs/deployment.md](docs/deployment.md)
|
|||
- Web: _this repository_
|
||||
- Command-line: [`ffsend`](https://github.com/timvisee/ffsend)
|
||||
- Android: _see [Android](#android) section_
|
||||
- Thunderbird: [FileLink provider for Send](https://addons.thunderbird.net/thunderbird/addon/filelink-provider-for-send/)
|
||||
|
||||
#### Android
|
||||
|
||||
|
|
|
@ -77,7 +77,11 @@ function body(main) {
|
|||
state.capabilities = {
|
||||
account: true
|
||||
}; //TODO
|
||||
state.archive = new Archive([], DEFAULTS.EXPIRE_SECONDS);
|
||||
state.archive = new Archive(
|
||||
[],
|
||||
DEFAULTS.EXPIRE_SECONDS,
|
||||
DEFAULTS.DOWNLOADS
|
||||
);
|
||||
state.storage = storage;
|
||||
state.user = new User(storage, LIMITS);
|
||||
state.sentry = Sentry;
|
||||
|
|
|
@ -43,7 +43,7 @@ function post(obj, bearerToken) {
|
|||
'Content-Type': 'application/json'
|
||||
};
|
||||
if (bearerToken) {
|
||||
h['Authentication'] = `Bearer ${bearerToken}`;
|
||||
h['Authorization'] = `Bearer ${bearerToken}`;
|
||||
}
|
||||
return {
|
||||
method: 'POST',
|
||||
|
|
|
@ -14,11 +14,12 @@ function isDupe(newFile, array) {
|
|||
}
|
||||
|
||||
export default class Archive {
|
||||
constructor(files = [], defaultTimeLimit = 86400) {
|
||||
constructor(files = [], defaultTimeLimit = 86400, defaultDownloadLimit = 1) {
|
||||
this.files = Array.from(files);
|
||||
this.defaultTimeLimit = defaultTimeLimit;
|
||||
this.defaultDownloadLimit = defaultDownloadLimit;
|
||||
this.timeLimit = defaultTimeLimit;
|
||||
this.dlimit = 1;
|
||||
this.dlimit = defaultDownloadLimit;
|
||||
this.password = null;
|
||||
}
|
||||
|
||||
|
@ -76,7 +77,7 @@ export default class Archive {
|
|||
|
||||
clear() {
|
||||
this.files = [];
|
||||
this.dlimit = 1;
|
||||
this.dlimit = this.defaultDownloadLimit;
|
||||
this.timeLimit = this.defaultTimeLimit;
|
||||
this.password = null;
|
||||
}
|
||||
|
|
|
@ -7,7 +7,7 @@ const experiments = {
|
|||
return true;
|
||||
},
|
||||
variant: function() {
|
||||
return ['white-blue', 'blue', 'white-violet', 'violet'][
|
||||
return ['white-primary', 'primary', 'white-violet', 'violet'][
|
||||
Math.floor(Math.random() * 4)
|
||||
];
|
||||
},
|
||||
|
|
|
@ -224,24 +224,6 @@ async function saveFile(file) {
|
|||
if (navigator.msSaveBlob) {
|
||||
navigator.msSaveBlob(blob, file.name);
|
||||
return resolve();
|
||||
} else if (/iPhone|fxios/i.test(navigator.userAgent)) {
|
||||
// This method is much slower but createObjectURL
|
||||
// is buggy on iOS
|
||||
const reader = new FileReader();
|
||||
reader.addEventListener('loadend', function() {
|
||||
if (reader.error) {
|
||||
return reject(reader.error);
|
||||
}
|
||||
if (reader.result) {
|
||||
const a = document.createElement('a');
|
||||
a.href = reader.result;
|
||||
a.download = file.name;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
}
|
||||
resolve();
|
||||
});
|
||||
reader.readAsDataURL(blob);
|
||||
} else {
|
||||
const downloadUrl = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import { FluentBundle } from '@fluent/bundle';
|
||||
import { FluentBundle, FluentResource } from '@fluent/bundle';
|
||||
|
||||
function makeBundle(locale, ftl) {
|
||||
const bundle = new FluentBundle(locale, { useIsolating: false });
|
||||
bundle.addMessages(ftl);
|
||||
bundle.addResource(new FluentResource(ftl));
|
||||
return bundle;
|
||||
}
|
||||
|
||||
|
@ -19,7 +19,7 @@ export async function getTranslator(locale) {
|
|||
return function(id, data) {
|
||||
for (let bundle of bundles) {
|
||||
if (bundle.hasMessage(id)) {
|
||||
return bundle.format(bundle.getMessage(id), data);
|
||||
return bundle.formatPattern(bundle.getMessage(id).value, data);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
129
app/main.css
|
@ -7,17 +7,14 @@ html {
|
|||
@tailwind components;
|
||||
|
||||
:not(input) {
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
:root {
|
||||
--violet-gradient: linear-gradient(
|
||||
-180deg,
|
||||
rgba(144, 89, 255, 0.8) 0%,
|
||||
rgba(144, 89, 255, 0.4) 100%
|
||||
rgb(144 89 255 / 80%) 0%,
|
||||
rgb(144 89 255 / 40%) 100%
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -39,7 +36,7 @@ body {
|
|||
}
|
||||
|
||||
.btn {
|
||||
@apply bg-blue-60;
|
||||
@apply bg-primary;
|
||||
@apply text-white;
|
||||
@apply cursor-pointer;
|
||||
@apply py-4;
|
||||
|
@ -48,11 +45,11 @@ body {
|
|||
}
|
||||
|
||||
.btn:hover {
|
||||
@apply bg-blue-70;
|
||||
@apply bg-primary_accent;
|
||||
}
|
||||
|
||||
.btn:focus {
|
||||
@apply bg-blue-70;
|
||||
@apply bg-primary_accent;
|
||||
}
|
||||
|
||||
.checkbox {
|
||||
|
@ -71,7 +68,7 @@ body {
|
|||
|
||||
.checkbox > label::before {
|
||||
/* @apply bg-grey-10; */
|
||||
@apply border;
|
||||
@apply border-default;
|
||||
@apply rounded-sm;
|
||||
|
||||
content: '';
|
||||
|
@ -82,16 +79,16 @@ body {
|
|||
}
|
||||
|
||||
.checkbox > label:hover::before {
|
||||
@apply border-blue-50;
|
||||
@apply border-primary;
|
||||
}
|
||||
|
||||
.checkbox > input:focus + label::before {
|
||||
@apply border-blue-50;
|
||||
@apply border-primary;
|
||||
}
|
||||
|
||||
.checkbox > input:checked + label::before {
|
||||
@apply bg-blue-50;
|
||||
@apply border-blue-50;
|
||||
@apply bg-primary;
|
||||
@apply border-primary;
|
||||
|
||||
background-image: url('../assets/lock.svg');
|
||||
background-position: center;
|
||||
|
@ -104,8 +101,8 @@ body {
|
|||
}
|
||||
|
||||
.checkbox > input:disabled + label::before {
|
||||
@apply bg-blue-50;
|
||||
@apply border-blue-50;
|
||||
@apply bg-primary;
|
||||
@apply border-primary;
|
||||
|
||||
background-image: url('../assets/lock.svg');
|
||||
background-position: center;
|
||||
|
@ -118,7 +115,7 @@ details {
|
|||
overflow: hidden;
|
||||
}
|
||||
|
||||
details > summary::-webkit-details-marker {
|
||||
details > summary::marker {
|
||||
display: none;
|
||||
}
|
||||
|
||||
|
@ -153,16 +150,16 @@ footer li a:hover {
|
|||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.link-blue {
|
||||
@apply text-blue-60;
|
||||
.link-primary {
|
||||
@apply text-primary;
|
||||
}
|
||||
|
||||
.link-blue:hover {
|
||||
@apply text-blue-70;
|
||||
.link-primary:hover {
|
||||
@apply text-primary_accent;
|
||||
}
|
||||
|
||||
.link-blue:focus {
|
||||
@apply text-blue-70;
|
||||
.link-primary:focus {
|
||||
@apply text-primary_accent;
|
||||
}
|
||||
|
||||
.main-header img {
|
||||
|
@ -170,12 +167,27 @@ footer li a:hover {
|
|||
width: auto;
|
||||
}
|
||||
|
||||
.text-underline {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.d-block {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.d-inline-block {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.align-middle {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.main {
|
||||
display: flex;
|
||||
position: relative;
|
||||
max-width: 64rem;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.main > section {
|
||||
|
@ -205,19 +217,18 @@ progress::-webkit-progress-value {
|
|||
background-image: -webkit-linear-gradient(
|
||||
-45deg,
|
||||
transparent 20%,
|
||||
rgba(255, 255, 255, 0.4) 20%,
|
||||
rgba(255, 255, 255, 0.4) 40%,
|
||||
rgb(255 255 255 / 40%) 20%,
|
||||
rgb(255 255 255 / 40%) 40%,
|
||||
transparent 40%,
|
||||
transparent 60%,
|
||||
rgba(255, 255, 255, 0.4) 60%,
|
||||
rgba(255, 255, 255, 0.4) 80%,
|
||||
rgb(255 255 255 / 40%) 60%,
|
||||
rgb(255 255 255 / 40%) 80%,
|
||||
transparent 80%
|
||||
),
|
||||
-webkit-linear-gradient(left, #0a84ff, #0a84ff);
|
||||
-webkit-linear-gradient(left, var(--color-primary), var(--color-primary));
|
||||
/* stylelint-enable */
|
||||
border-radius: 2px;
|
||||
background-size: 21px 20px, 100% 100%, 100% 100%;
|
||||
-webkit-animation: animate-stripes 1s linear infinite;
|
||||
}
|
||||
|
||||
progress::-moz-progress-bar {
|
||||
|
@ -225,27 +236,21 @@ progress::-moz-progress-bar {
|
|||
background-image: -moz-linear-gradient(
|
||||
135deg,
|
||||
transparent 20%,
|
||||
rgba(255, 255, 255, 0.4) 20%,
|
||||
rgba(255, 255, 255, 0.4) 40%,
|
||||
rgb(255 255 255 / 40%) 20%,
|
||||
rgb(255 255 255 / 40%) 40%,
|
||||
transparent 40%,
|
||||
transparent 60%,
|
||||
rgba(255, 255, 255, 0.4) 60%,
|
||||
rgba(255, 255, 255, 0.4) 80%,
|
||||
rgb(255 255 255 / 40%) 60%,
|
||||
rgb(255 255 255 / 40%) 80%,
|
||||
transparent 80%
|
||||
),
|
||||
-moz-linear-gradient(left, #0a84ff, #0a84ff);
|
||||
-moz-linear-gradient(left, var(--color-primary), var(--color-primary));
|
||||
/* stylelint-enable */
|
||||
border-radius: 2px;
|
||||
background-size: 21px 20px, 100% 100%, 100% 100%;
|
||||
animation: animate-stripes 1s linear infinite;
|
||||
}
|
||||
|
||||
@-webkit-keyframes animate-stripes {
|
||||
100% {
|
||||
background-position: -21px 0;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes animate-stripes {
|
||||
100% {
|
||||
background-position: -21px 0;
|
||||
|
@ -271,7 +276,6 @@ select {
|
|||
@apply m-auto;
|
||||
@apply py-8;
|
||||
|
||||
min-height: 42rem;
|
||||
max-height: 42rem;
|
||||
width: calc(100% - 3rem);
|
||||
}
|
||||
|
@ -285,28 +289,28 @@ select {
|
|||
}
|
||||
|
||||
.btn {
|
||||
@apply bg-blue-40;
|
||||
@apply bg-primary;
|
||||
@apply text-white;
|
||||
}
|
||||
|
||||
.btn:hover {
|
||||
@apply bg-blue-50;
|
||||
@apply bg-primary_accent;
|
||||
}
|
||||
|
||||
.btn:focus {
|
||||
@apply bg-blue-50;
|
||||
@apply bg-primary_accent;
|
||||
}
|
||||
|
||||
.link-blue {
|
||||
@apply text-blue-40;
|
||||
.link-primary {
|
||||
@apply text-primary;
|
||||
}
|
||||
|
||||
.link-blue:hover {
|
||||
@apply text-blue-50;
|
||||
.link-primary:hover {
|
||||
@apply text-primary_accent;
|
||||
}
|
||||
|
||||
.link-blue:focus {
|
||||
@apply text-blue-50;
|
||||
.link-primary:focus {
|
||||
@apply text-primary_accent;
|
||||
}
|
||||
|
||||
.main > section {
|
||||
|
@ -315,7 +319,7 @@ select {
|
|||
|
||||
@screen md {
|
||||
.main > section {
|
||||
@apply border;
|
||||
@apply border-default;
|
||||
@apply border-grey-80;
|
||||
}
|
||||
}
|
||||
|
@ -325,13 +329,12 @@ select {
|
|||
|
||||
@responsive {
|
||||
.shadow-light {
|
||||
box-shadow: 0 0 8px 0 rgba(12, 12, 13, 0.1);
|
||||
box-shadow: 0 0 8px 0 rgb(12 12 13 / 10%);
|
||||
}
|
||||
|
||||
.shadow-big {
|
||||
box-shadow: 0 12px 18px 2px rgba(34, 0, 51, 0.04),
|
||||
0 6px 22px 4px rgba(7, 48, 114, 0.12),
|
||||
0 6px 10px -4px rgba(14, 13, 26, 0.12);
|
||||
box-shadow: 0 12px 18px 2px rgb(34 0 51 / 4%),
|
||||
0 6px 22px 4px rgb(7 48 114 / 12%), 0 6px 10px -4px rgb(14 13 26 / 12%);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -365,20 +368,20 @@ select {
|
|||
|
||||
/* begin signin button color experiment */
|
||||
|
||||
.white-blue {
|
||||
@apply border-blue-60;
|
||||
.white-primary {
|
||||
@apply border-primary;
|
||||
@apply border-2;
|
||||
@apply text-blue-60;
|
||||
@apply text-primary;
|
||||
}
|
||||
|
||||
.white-blue:hover,
|
||||
.white-blue:focus {
|
||||
@apply bg-blue-60;
|
||||
.white-primary:hover,
|
||||
.white-primary:focus {
|
||||
@apply bg-primary;
|
||||
@apply text-white;
|
||||
}
|
||||
|
||||
.blue {
|
||||
@apply bg-blue-60;
|
||||
.primary {
|
||||
@apply bg-primary;
|
||||
@apply text-white;
|
||||
}
|
||||
|
||||
|
|
|
@ -52,7 +52,7 @@ if (process.env.NODE_ENV === 'production') {
|
|||
DEFAULTS,
|
||||
WEB_UI,
|
||||
PREFS,
|
||||
archive: new Archive([], DEFAULTS.EXPIRE_SECONDS),
|
||||
archive: new Archive([], DEFAULTS.EXPIRE_SECONDS, DEFAULTS.DOWNLOADS),
|
||||
capabilities,
|
||||
translate,
|
||||
storage,
|
||||
|
|
1281
app/qrcode.js
|
@ -69,7 +69,7 @@ class Account extends Component {
|
|||
return html`
|
||||
<send-account>
|
||||
<button
|
||||
class="px-4 py-2 md:px-8 md:py-4 focus:outline signin border-2 link-blue border-blue-60 hover:border-blue-70 dark:border-blue-40 dark:hover:border-blue-50"
|
||||
class="px-4 py-2 md:px-8 md:py-4 focus:outline signin border-2 link-primary border-primary hover:border-primary dark:border-primary dark:hover:border-primary"
|
||||
onclick="${e => this.login(e)}"
|
||||
title="${translate('signInOnlyButton')}"
|
||||
>
|
||||
|
@ -83,19 +83,19 @@ class Account extends Component {
|
|||
<input
|
||||
type="image"
|
||||
alt="${user.email}"
|
||||
class="w-8 h-8 rounded-full border text-blue-50 md:text-white focus:outline"
|
||||
class="w-8 h-8 rounded-full border-default text-primary md:text-white focus:outline"
|
||||
src="${user.avatar}"
|
||||
onclick="${e => this.avatarClick(e)}"
|
||||
/>
|
||||
<ul
|
||||
id="accountMenu"
|
||||
class="invisible absolute top-0 right-0 mt-10 pt-2 pb-2 bg-white shadow-md whitespace-no-wrap outline-none z-50 dark:bg-grey-80"
|
||||
class="invisible absolute top-0 right-0 mt-10 pt-2 pb-2 bg-white shadow-md whitespace-nowrap outline-none z-50 dark:bg-grey-80"
|
||||
onblur="${e => this.hideMenu(e)}"
|
||||
>
|
||||
<li class="p-2 text-grey-60 dark:text-grey-50">${user.email}</li>
|
||||
<li>
|
||||
<button
|
||||
class="block w-full text-left px-4 py-2 text-grey-80 dark:text-grey-30 hover:bg-blue-50 hover:text-white cursor-pointer focus:outline"
|
||||
class="block w-full text-left px-4 py-2 text-grey-80 dark:text-grey-30 hover:bg-primary hover:text-white cursor-pointer focus:outline"
|
||||
onclick="${e => this.logout(e)}"
|
||||
title="${translate('signOut')}"
|
||||
>
|
||||
|
|
|
@ -26,7 +26,7 @@ function expiryInfo(translate, archive) {
|
|||
}
|
||||
|
||||
function password(state) {
|
||||
const MAX_LENGTH = 32;
|
||||
const MAX_LENGTH = 4096;
|
||||
|
||||
return html`
|
||||
<div class="mb-2 px-1">
|
||||
|
@ -53,7 +53,7 @@ function password(state) {
|
|||
id="password-input"
|
||||
class="${state.archive.password
|
||||
? ''
|
||||
: 'invisible'} border rounded focus:border-blue-60 leading-normal my-1 py-1 px-2 h-8 dark:bg-grey-80"
|
||||
: 'invisible'} border-default rounded-default focus:border-primary leading-normal my-1 py-1 px-2 h-8 dark:bg-grey-80"
|
||||
autocomplete="off"
|
||||
maxlength="${MAX_LENGTH}"
|
||||
type="password"
|
||||
|
@ -150,7 +150,7 @@ function password(state) {
|
|||
function fileInfo(file, action) {
|
||||
return html`
|
||||
<send-file class="flex flex-row items-center p-3 w-full">
|
||||
<svg class="h-8 w-8 text-white dark:text-grey-90">
|
||||
<svg class="h-8 w-8 text-primary">
|
||||
<use xlink:href="${assets.get('blue_file.svg')}#icon"/>
|
||||
</svg>
|
||||
<p class="ml-4 w-full">
|
||||
|
@ -166,7 +166,7 @@ function fileInfo(file, action) {
|
|||
function archiveInfo(archive, action) {
|
||||
return html`
|
||||
<p class="w-full flex items-center">
|
||||
<svg class="h-8 w-6 mr-3 flex-shrink-0 text-white dark:text-grey-90">
|
||||
<svg class="h-8 w-6 mr-3 flex-shrink-0 text-primary">
|
||||
<use xlink:href="${assets.get('blue_file.svg')}#icon"/>
|
||||
</svg>
|
||||
<p class="flex-grow">
|
||||
|
@ -188,7 +188,7 @@ function archiveDetails(translate, archive) {
|
|||
ontoggle="${toggled}"
|
||||
>
|
||||
<summary
|
||||
class="flex items-center link-blue text-sm cursor-pointer outline-none"
|
||||
class="flex items-center link-primary text-sm cursor-pointer outline-none"
|
||||
>
|
||||
<svg
|
||||
class="fill-current w-4 h-4 mr-1"
|
||||
|
@ -218,7 +218,7 @@ module.exports = function(state, emit, archive) {
|
|||
state.capabilities.share || platform() === 'android'
|
||||
? html`
|
||||
<button
|
||||
class="link-blue self-end flex items-start"
|
||||
class="link-primary self-end flex items-start"
|
||||
onclick=${share}
|
||||
title="Share link"
|
||||
>
|
||||
|
@ -230,7 +230,7 @@ module.exports = function(state, emit, archive) {
|
|||
`
|
||||
: html`
|
||||
<button
|
||||
class="link-blue focus:outline self-end flex items-center"
|
||||
class="link-primary focus:outline self-end flex items-center"
|
||||
onclick=${copy}
|
||||
title="${state.translate('copyLinkButton')}"
|
||||
>
|
||||
|
@ -244,7 +244,7 @@ module.exports = function(state, emit, archive) {
|
|||
platform() === 'web'
|
||||
? html`
|
||||
<a
|
||||
class="flex items-baseline link-blue"
|
||||
class="flex items-baseline link-primary"
|
||||
href="${archive.url}"
|
||||
title="${state.translate('downloadButtonLabel')}"
|
||||
tabindex="0"
|
||||
|
@ -261,7 +261,7 @@ module.exports = function(state, emit, archive) {
|
|||
return html`
|
||||
<send-archive
|
||||
id="archive-${archive.id}"
|
||||
class="flex flex-col items-start rounded shadow-light bg-white p-4 w-full dark:bg-grey-90 dark:border dark:border-grey-70"
|
||||
class="flex flex-col items-start rounded-default shadow-light bg-white p-4 w-full dark:bg-grey-90 dark:border-default dark:border-grey-70"
|
||||
>
|
||||
${archiveInfo(
|
||||
archive,
|
||||
|
@ -335,7 +335,7 @@ module.exports.wip = function(state, emit) {
|
|||
fileInfo(f, remove(f, state.translate('deleteButtonHover')))
|
||||
),
|
||||
'flex-shrink bg-grey-10 rounded-t overflow-y-auto px-6 py-4 md:h-full md:max-h-half-screen dark:bg-black',
|
||||
'bg-white px-2 my-2 shadow-light rounded dark:bg-grey-90 dark:border dark:border-grey-80'
|
||||
'bg-white px-2 my-2 shadow-light rounded-default dark:bg-grey-90 dark:border-default dark:border-grey-80'
|
||||
)}
|
||||
<div
|
||||
class="flex-shrink-0 flex-grow flex items-end p-4 bg-grey-10 rounded-b mb-1 font-medium dark:bg-grey-90"
|
||||
|
@ -358,7 +358,7 @@ module.exports.wip = function(state, emit) {
|
|||
class="flex items-center cursor-pointer"
|
||||
title="${state.translate('addFilesButton')}"
|
||||
>
|
||||
<svg class="w-6 h-6 mr-2 link-blue">
|
||||
<svg class="w-6 h-6 mr-2 link-primary">
|
||||
<use xlink:href="${assets.get('addfiles.svg')}#plus" />
|
||||
</svg>
|
||||
${state.translate('addFilesButton')}
|
||||
|
@ -438,7 +438,7 @@ module.exports.uploading = function(state, emit) {
|
|||
return html`
|
||||
<send-upload-area
|
||||
id="${archive.id}"
|
||||
class="flex flex-col items-start rounded shadow-light bg-white p-4 w-full dark:bg-grey-90"
|
||||
class="flex flex-col items-start rounded-default shadow-light bg-white p-4 w-full dark:bg-grey-90"
|
||||
>
|
||||
${archiveInfo(archive)}
|
||||
<div class="text-xs opacity-75 w-full mt-2 mb-2">
|
||||
|
@ -448,12 +448,12 @@ module.exports.uploading = function(state, emit) {
|
|||
expiresAt: Date.now() + 500 + state.archive.timeLimit * 1000
|
||||
})}
|
||||
</div>
|
||||
<div class="link-blue text-sm font-medium mt-2">
|
||||
<div class="link-primary text-sm font-medium mt-2">
|
||||
${progressPercent}
|
||||
</div>
|
||||
<progress class="my-3" value="${progress}">${progressPercent}</progress>
|
||||
<button
|
||||
class="link-blue self-end font-medium"
|
||||
class="link-primary self-end font-medium"
|
||||
onclick=${cancel}
|
||||
title="${state.translate('deletePopupCancel')}"
|
||||
>
|
||||
|
@ -475,7 +475,7 @@ module.exports.empty = function(state, emit) {
|
|||
? ''
|
||||
: html`
|
||||
<button
|
||||
class="center font-medium text-sm link-blue mt-4 mb-2"
|
||||
class="center font-medium text-sm link-primary mt-4 mb-2"
|
||||
onclick="${event => {
|
||||
event.stopPropagation();
|
||||
emit('signup-cta', 'drop');
|
||||
|
@ -486,16 +486,26 @@ module.exports.empty = function(state, emit) {
|
|||
})}
|
||||
</button>
|
||||
`;
|
||||
const uploadNotice = state.WEB_UI.UPLOAD_AREA_NOTICE_HTML
|
||||
? html`
|
||||
<p
|
||||
class="w-full mt-8 p-2 border-default dark:border-grey-70 rounded-default text-orange-60 bg-yellow-40 text-center leading-normal"
|
||||
>
|
||||
${raw(state.WEB_UI.UPLOAD_AREA_NOTICE_HTML)}
|
||||
</p>
|
||||
`
|
||||
: '';
|
||||
|
||||
return html`
|
||||
<send-upload-area
|
||||
class="flex flex-col items-center justify-center border-2 border-dashed border-grey-transparent rounded px-6 py-16 h-full w-full dark:border-grey-60"
|
||||
class="flex flex-col items-center justify-center border-2 border-dashed border-grey-transparent rounded-default px-6 py-16 h-full w-full dark:border-grey-60"
|
||||
onclick="${e => {
|
||||
if (e.target.tagName !== 'LABEL') {
|
||||
document.getElementById('file-upload').click();
|
||||
}
|
||||
}}"
|
||||
>
|
||||
<svg class="w-10 h-10 link-blue">
|
||||
<svg class="w-10 h-10 link-primary">
|
||||
<use xlink:href="/${assets.get('addfiles.svg')}#plus" />
|
||||
</svg>
|
||||
<div class="pt-6 pb-2 text-center text-lg font-bold tracking-wide">
|
||||
|
@ -526,16 +536,16 @@ module.exports.empty = function(state, emit) {
|
|||
>
|
||||
${state.translate('addFilesButton')}
|
||||
</label>
|
||||
${upsell}
|
||||
${upsell} ${uploadNotice}
|
||||
</send-upload-area>
|
||||
`;
|
||||
|
||||
function focus(event) {
|
||||
event.target.nextElementSibling.classList.add('bg-blue-70', 'outline');
|
||||
event.target.nextElementSibling.classList.add('bg-primary', 'outline');
|
||||
}
|
||||
|
||||
function blur(event) {
|
||||
event.target.nextElementSibling.classList.remove('bg-blue-70', 'outline');
|
||||
event.target.nextElementSibling.classList.remove('bg-primary', 'outline');
|
||||
}
|
||||
|
||||
function add(event) {
|
||||
|
@ -559,11 +569,43 @@ module.exports.preview = function(state, emit) {
|
|||
${archiveDetails(state.translate, archive)}
|
||||
</div>
|
||||
`;
|
||||
const notice = state.WEB_UI.DOWNLOAD_NOTICE_HTML
|
||||
? html`
|
||||
<p
|
||||
class="w-full mt-4 p-2 border-default dark:border-grey-70 rounded-default text-orange-60 bg-yellow-40 text-center leading-normal"
|
||||
>
|
||||
${raw(state.WEB_UI.DOWNLOAD_NOTICE_HTML)}
|
||||
</p>
|
||||
`
|
||||
: '';
|
||||
const sponsor = state.WEB_UI.SHOW_THUNDERBIRD_SPONSOR
|
||||
? html`
|
||||
<a
|
||||
class="w-full mt-5 mb-2 p-2 border-default dark:border-grey-70 rounded-default text-orange-60 bg-yellow-40 text-center leading-normal"
|
||||
href="https://www.thunderbird.net/"
|
||||
>
|
||||
<svg
|
||||
width="30"
|
||||
height="30"
|
||||
class="m-2 mr-3 d-inline-block align-middle"
|
||||
>
|
||||
<image
|
||||
xlink:href="${assets.get('thunderbird-icon.svg')}"
|
||||
src="${assets.get('thunderbird-icon.svg')}"
|
||||
width="30"
|
||||
height="30"
|
||||
/>
|
||||
</svg>
|
||||
${state.translate('sponsoredByThunderbird')}
|
||||
</a>
|
||||
`
|
||||
: '';
|
||||
|
||||
return html`
|
||||
<send-archive
|
||||
class="flex flex-col max-h-full bg-white p-4 w-full md:w-128 dark:bg-grey-90"
|
||||
>
|
||||
<div class="border rounded py-3 px-6 dark:border-grey-70">
|
||||
<div class="border-default rounded-default py-3 px-6 dark:border-grey-70">
|
||||
${archiveInfo(archive)} ${details}
|
||||
</div>
|
||||
<button
|
||||
|
@ -574,6 +616,7 @@ module.exports.preview = function(state, emit) {
|
|||
>
|
||||
${state.translate('downloadButtonLabel')}
|
||||
</button>
|
||||
${notice} ${sponsor}
|
||||
</send-archive>
|
||||
`;
|
||||
|
||||
|
@ -590,10 +633,10 @@ module.exports.downloading = function(state) {
|
|||
const progressPercent = percent(progress);
|
||||
return html`
|
||||
<send-archive
|
||||
class="flex flex-col bg-white rounded shadow-light p-4 w-full max-w-sm md:w-128 dark:bg-grey-90"
|
||||
class="flex flex-col bg-white rounded-default shadow-light p-4 w-full max-w-sm md:w-128 dark:bg-grey-90"
|
||||
>
|
||||
${archiveInfo(archive)}
|
||||
<div class="link-blue text-sm font-medium mt-2">
|
||||
<div class="link-primary text-sm font-medium mt-2">
|
||||
${progressPercent}
|
||||
</div>
|
||||
<progress class="my-3" value="${progress}">${progressPercent}</progress>
|
||||
|
|
|
@ -21,7 +21,7 @@ module.exports = function(name, url) {
|
|||
<input
|
||||
type="text"
|
||||
id="share-url"
|
||||
class="block w-full my-4 border rounded-lg leading-loose h-12 px-2 py-1 dark:bg-grey-80"
|
||||
class="block w-full my-4 border-default rounded-lg leading-loose h-12 px-2 py-1 dark:bg-grey-80"
|
||||
value="${url}"
|
||||
readonly="true"
|
||||
/>
|
||||
|
@ -42,7 +42,7 @@ module.exports = function(name, url) {
|
|||
${state.translate('copyLinkButton')}
|
||||
</button>
|
||||
<button
|
||||
class="link-blue my-4 font-medium cursor-pointer focus:outline"
|
||||
class="link-primary my-4 font-medium cursor-pointer focus:outline"
|
||||
onclick="${close}"
|
||||
title="${state.translate('okButton')}"
|
||||
>
|
||||
|
|
|
@ -11,7 +11,9 @@ module.exports = function(state) {
|
|||
<h1 class="text-center text-3xl font-bold my-2">
|
||||
${state.translate('downloadFinish')}
|
||||
</h1>
|
||||
<img src="${assets.get('completed.svg')}" class="my-8 h-48" />
|
||||
<svg class="my-8 h-48 text-primary">
|
||||
<use xlink:href="${assets.get('completed.svg')}#Page-1" />
|
||||
</svg>
|
||||
<p
|
||||
class="text-grey-80 leading-normal dark:text-grey-40 ${state.user
|
||||
.loggedIn
|
||||
|
|
|
@ -17,7 +17,7 @@ module.exports = function(state, emit) {
|
|||
${state.translate('downloadDescription')}
|
||||
</p>
|
||||
<form
|
||||
class="flex flex-row flex-no-wrap w-full md:w-4/5"
|
||||
class="flex flex-row flex-nowrap w-full md:w-4/5"
|
||||
onsubmit="${checkPassword}"
|
||||
data-no-csrf
|
||||
>
|
||||
|
@ -32,7 +32,7 @@ module.exports = function(state, emit) {
|
|||
class="w-full border-l border-t border-b rounded-l-lg rounded-r-none ${invalid
|
||||
? 'border-red dark:border-red-40'
|
||||
: 'border-grey'} leading-loose px-2 py-1 dark:bg-grey-80"
|
||||
maxlength="32"
|
||||
maxlength="4096"
|
||||
autocomplete="off"
|
||||
placeholder="${state.translate('unlockInputPlaceholder')}"
|
||||
oninput="${inputChanged}"
|
||||
|
|
|
@ -13,7 +13,9 @@ module.exports = function(state, emit) {
|
|||
<h1 class="text-center text-3xl font-bold my-2">
|
||||
${state.translate('errorPageHeader')}
|
||||
</h1>
|
||||
<img class="my-12 h-48" src="${assets.get('error.svg')}" />
|
||||
<svg class="text-primary my-12 h-48">
|
||||
<use xlink:href="${assets.get('error.svg')}#svg114" />
|
||||
</svg>
|
||||
<p
|
||||
class="max-w-md text-center text-grey-80 leading-normal dark:text-grey-40 ${state
|
||||
.user.loggedIn
|
||||
|
|
|
@ -31,12 +31,11 @@ module.exports = function(state, emit) {
|
|||
counts,
|
||||
num => state.translate('downloadCount', { num }),
|
||||
value => {
|
||||
const max = state.user.maxDownloads;
|
||||
state.archive.dlimit = Math.min(value, max);
|
||||
if (value > max) {
|
||||
emit('signup-cta', 'count');
|
||||
} else {
|
||||
emit('render');
|
||||
const selected = parseInt(value);
|
||||
state.archive.dlimit = selected;
|
||||
emit('render');
|
||||
if (selected > parseInt(state.user.maxDownloads || '0')) {
|
||||
console.log('Chosen max download count is larger than the allowed limit', selected)
|
||||
}
|
||||
},
|
||||
'expire-after-dl-count-select'
|
||||
|
@ -58,12 +57,11 @@ module.exports = function(state, emit) {
|
|||
return state.translate(l10n.id, l10n);
|
||||
},
|
||||
value => {
|
||||
const max = state.user.maxExpireSeconds;
|
||||
state.archive.timeLimit = Math.min(value, max);
|
||||
if (value > max) {
|
||||
emit('signup-cta', 'time');
|
||||
} else {
|
||||
emit('render');
|
||||
const selected = parseInt(value);
|
||||
state.archive.timeLimit = selected;
|
||||
emit('render');
|
||||
if (selected > parseInt(state.user.maxExpireSeconds || '0')) {
|
||||
console.log('Chosen download expiration is larger than the allowed limit', selected)
|
||||
}
|
||||
},
|
||||
'expire-after-time-select'
|
||||
|
|
|
@ -1,9 +1,11 @@
|
|||
/*global WEB_UI*/
|
||||
|
||||
const { platform } = require('../utils');
|
||||
const assets = require('../../common/assets');
|
||||
|
||||
const size = 32;
|
||||
const loaderWidth = 5;
|
||||
const loaderColor = '#0090ed';
|
||||
const loaderColor = WEB_UI.COLORS.PRIMARY;
|
||||
|
||||
function drawCircle(canvas, context, color, lineWidth, outerWidth, percent) {
|
||||
canvas.width = canvas.height = outerWidth;
|
||||
|
@ -32,7 +34,10 @@ module.exports.updateFavicon = function(progressRatio) {
|
|||
const progress = progressRatio * 100;
|
||||
if (progress === 0 || progress === 100) {
|
||||
link.type = 'image/png';
|
||||
link.href = assets.get('favicon-32x32.png');
|
||||
link.href =
|
||||
WEB_UI.CUSTOM_ASSETS.favicon_32px !== ''
|
||||
? WEB_UI.CUSTOM_ASSETS.favicon_32px
|
||||
: assets.get('favicon-32x32.png');
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
@ -65,6 +65,45 @@ class Footer extends Component {
|
|||
`);
|
||||
}
|
||||
|
||||
// Defining a custom footer
|
||||
var footer = [];
|
||||
if (this.state != undefined && this.state.WEB_UI != undefined) {
|
||||
const WEB_UI = this.state.WEB_UI;
|
||||
|
||||
if (WEB_UI.CUSTOM_FOOTER_URL != '' && WEB_UI.CUSTOM_FOOTER_TEXT != '') {
|
||||
footer.push(html`
|
||||
<li class="m-2">
|
||||
<a href="${WEB_UI.CUSTOM_FOOTER_URL}" target="_blank">
|
||||
${WEB_UI.CUSTOM_FOOTER_TEXT}
|
||||
</a>
|
||||
</li>
|
||||
`);
|
||||
}
|
||||
else if (WEB_UI.CUSTOM_FOOTER_URL != '') {
|
||||
footer.push(html`
|
||||
<li class="m-2">
|
||||
<a href="${WEB_UI.CUSTOM_FOOTER_URL}" target="_blank">
|
||||
${WEB_UI.CUSTOM_FOOTER_URL}
|
||||
</a>
|
||||
</li>
|
||||
`);
|
||||
}
|
||||
else if (WEB_UI.CUSTOM_FOOTER_TEXT != '') {
|
||||
footer.push(html`
|
||||
<li class="m-2">
|
||||
${WEB_UI.CUSTOM_FOOTER_TEXT}
|
||||
</li>
|
||||
`)
|
||||
}
|
||||
else {
|
||||
footer.push(html`
|
||||
<li class="m-2">
|
||||
${translate('footerText')}
|
||||
</li>
|
||||
`);
|
||||
}
|
||||
}
|
||||
|
||||
return html`
|
||||
<footer
|
||||
class="flex flex-col md:flex-row items-start w-full flex-none self-start p-6 md:p-8 font-medium text-xs text-grey-60 dark:text-grey-40 md:items-center justify-between"
|
||||
|
@ -72,7 +111,7 @@ class Footer extends Component {
|
|||
<ul
|
||||
class="flex flex-col md:flex-row items-start md:items-center md:justify-start"
|
||||
>
|
||||
<li class="m-2">${translate('footerText')}</li>
|
||||
${footer}
|
||||
</ul>
|
||||
<ul
|
||||
class="flex flex-col md:flex-row items-start md:items-center md:justify-end"
|
||||
|
|
|
@ -18,13 +18,26 @@ class Header extends Component {
|
|||
}
|
||||
|
||||
createElement() {
|
||||
let assetMap = {};
|
||||
if (this.state.ui !== undefined) assetMap = this.state.ui.assets;
|
||||
else
|
||||
assetMap = {
|
||||
icon:
|
||||
this.state.WEB_UI.CUSTOM_ASSETS.icon !== ''
|
||||
? this.state.WEB_UI.CUSTOM_ASSETS.icon
|
||||
: assets.get('icon.svg'),
|
||||
wordmark:
|
||||
this.state.WEB_UI.CUSTOM_ASSETS.wordmark !== ''
|
||||
? this.state.WEB_UI.CUSTOM_ASSETS.wordmark
|
||||
: assets.get('wordmark.svg') + '#logo'
|
||||
};
|
||||
const title =
|
||||
platform() === 'android'
|
||||
? html`
|
||||
<a class="flex flex-row items-center">
|
||||
<img src="${assets.get('icon.svg')}" />
|
||||
<img src="${assetMap.icon}" />
|
||||
<svg class="w-48">
|
||||
<use xlink:href="${assets.get('wordmark.svg')}#logo" />
|
||||
<use xlink:href="${assetMap.wordmark}" />
|
||||
</svg>
|
||||
</a>
|
||||
`
|
||||
|
@ -32,10 +45,10 @@ class Header extends Component {
|
|||
<a class="flex flex-row items-center" href="/">
|
||||
<img
|
||||
alt="${this.state.translate('title')}"
|
||||
src="${assets.get('icon.svg')}"
|
||||
src="${assetMap.icon}"
|
||||
/>
|
||||
<svg viewBox="66 0 340 64" class="w-48 md:w-64">
|
||||
<use xlink:href="${assets.get('wordmark.svg')}#logo" />
|
||||
<use xlink:href="${assetMap.wordmark}" />
|
||||
</svg>
|
||||
</a>
|
||||
`;
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
const html = require('choo/html');
|
||||
const raw = require('choo/html/raw');
|
||||
const { list } = require('../utils');
|
||||
const archiveTile = require('./archiveTile');
|
||||
const modal = require('./modal');
|
||||
const intro = require('./intro');
|
||||
const assets = require('../../common/assets');
|
||||
|
||||
module.exports = function(state, emit) {
|
||||
const archives = state.storage.files
|
||||
|
@ -16,7 +18,42 @@ module.exports = function(state, emit) {
|
|||
} else {
|
||||
left = archiveTile.empty(state, emit);
|
||||
}
|
||||
|
||||
if (archives.length > 0 && state.WEB_UI.UPLOADS_LIST_NOTICE_HTML) {
|
||||
archives.push(html`
|
||||
<p
|
||||
class="w-full p-2 border-default dark:border-grey-70 rounded-default text-orange-60 bg-yellow-40 text-center leading-normal"
|
||||
>
|
||||
${raw(state.WEB_UI.UPLOADS_LIST_NOTICE_HTML)}
|
||||
</p>
|
||||
`);
|
||||
}
|
||||
|
||||
archives.reverse();
|
||||
|
||||
if (archives.length > 0 && state.WEB_UI.SHOW_THUNDERBIRD_SPONSOR) {
|
||||
archives.push(html`
|
||||
<a
|
||||
class="w-full p-2 border-default dark:border-grey-70 rounded-default text-orange-60 bg-yellow-40 text-center leading-normal d-block"
|
||||
href="https://www.thunderbird.net/"
|
||||
>
|
||||
<svg
|
||||
width="30"
|
||||
height="30"
|
||||
class="m-2 mr-3 d-inline-block align-middle"
|
||||
>
|
||||
<image
|
||||
xlink:href="${assets.get('thunderbird-icon.svg')}"
|
||||
src="${assets.get('thunderbird-icon.svg')}"
|
||||
width="30"
|
||||
height="30"
|
||||
/>
|
||||
</svg>
|
||||
Sponsored by Thunderbird
|
||||
</a>
|
||||
`);
|
||||
}
|
||||
|
||||
const right =
|
||||
archives.length === 0
|
||||
? intro(state)
|
||||
|
|
|
@ -1,10 +1,46 @@
|
|||
const html = require('choo/html');
|
||||
const raw = require('choo/html/raw');
|
||||
const assets = require('../../common/assets');
|
||||
|
||||
module.exports = function intro(state) {
|
||||
const notice = state.WEB_UI.MAIN_NOTICE_HTML
|
||||
? html`
|
||||
<p
|
||||
class="w-full mt-2 p-2 border-default dark:border-grey-70 rounded-default text-orange-60 bg-yellow-40 text-center leading-normal"
|
||||
>
|
||||
${raw(state.WEB_UI.MAIN_NOTICE_HTML)}
|
||||
</p>
|
||||
`
|
||||
: '';
|
||||
|
||||
const sponsor = state.WEB_UI.SHOW_THUNDERBIRD_SPONSOR
|
||||
? html`
|
||||
<a
|
||||
class="w-full mt-5 mb-2 p-2 border-default dark:border-grey-70 rounded-default text-orange-60 bg-yellow-40 text-center leading-normal"
|
||||
href="https://www.thunderbird.net/"
|
||||
>
|
||||
<svg
|
||||
width="30"
|
||||
height="30"
|
||||
class="m-2 mr-3 d-inline-block align-middle"
|
||||
>
|
||||
<image
|
||||
xlink:href="${assets.get('thunderbird-icon.svg')}"
|
||||
src="${assets.get('thunderbird-icon.svg')}"
|
||||
width="30"
|
||||
height="30"
|
||||
/>
|
||||
</svg>
|
||||
Sponsored by Thunderbird
|
||||
</a>
|
||||
`
|
||||
: '';
|
||||
|
||||
return html`
|
||||
<send-intro
|
||||
class="flex flex-col items-center justify-center bg-white px-6 md:py-0 py-6 mb-0 h-full w-full dark:bg-grey-90"
|
||||
>
|
||||
${notice}
|
||||
<div class="mt-12 flex flex-col h-full">
|
||||
<h1 class="text-3xl font-bold md:pb-2">
|
||||
${state.translate('introTitle')}
|
||||
|
@ -13,6 +49,7 @@ module.exports = function intro(state) {
|
|||
${state.translate('introDescription')}
|
||||
</p>
|
||||
</div>
|
||||
${sponsor}
|
||||
</send-intro>
|
||||
`;
|
||||
};
|
||||
|
|
|
@ -12,14 +12,14 @@ module.exports = function(state, emit) {
|
|||
'downloadTitle'
|
||||
)}</h1>
|
||||
<p
|
||||
class="w-full p-2 border border-yellow-50 rounded md:w-4/5 text-orange-60 bg-yellow-40 text-center leading-normal"
|
||||
class="w-full p-2 border-default border-yellow-50 rounded-default md:w-4/5 text-orange-60 bg-yellow-40 text-center leading-normal"
|
||||
>
|
||||
⚠️ ${state.translate('noStreamsWarning')} ⚠️
|
||||
</p>
|
||||
<form class="md:w-128" onsubmit=${submit}>
|
||||
<fieldset class="border rounded p-4 my-4" onchange=${optionChanged}>
|
||||
<fieldset class="border-default rounded-default p-4 my-4" onchange=${optionChanged}>
|
||||
<div class="flex items-center mb-2">
|
||||
<svg class="h-8 w-6 mr-3 flex-shrink-0 text-white dark:text-grey-90">
|
||||
<svg class="h-8 w-6 mr-3 flex-shrink-0 text-primary">
|
||||
<use xlink:href="${assets.get('blue_file.svg')}#icon"/>
|
||||
</svg>
|
||||
<p class="flex-grow">
|
||||
|
|
|
@ -13,7 +13,9 @@ module.exports = function(state, emit) {
|
|||
<h1 class="text-center text-3xl font-bold my-2">
|
||||
${state.translate('expiredTitle')}
|
||||
</h1>
|
||||
<img src="${assets.get('notFound.svg')}" class="my-12" />
|
||||
<svg class="text-primary my-12">
|
||||
<use xlink:href="${assets.get('notFound.svg')}#svg124" />
|
||||
</svg>
|
||||
<p
|
||||
class="max-w-md text-center text-grey-80 leading-normal dark:text-grey-40 ${state
|
||||
.user.loggedIn
|
||||
|
|
|
@ -2,7 +2,7 @@ const raw = require('choo/html/raw');
|
|||
const qrcode = require('../qrcode');
|
||||
|
||||
module.exports = function(url) {
|
||||
const gen = qrcode(5, 'L');
|
||||
const gen = qrcode(0, 'L');
|
||||
gen.addData(url);
|
||||
gen.make();
|
||||
const qr = gen.createSvgTag({ scalable: true });
|
||||
|
|
|
@ -1,32 +1,35 @@
|
|||
const html = require('choo/html');
|
||||
|
||||
module.exports = function(selected, options, translate, changed, htmlId) {
|
||||
let x = selected;
|
||||
function choose(event) {
|
||||
if (event.target.value != selected) {
|
||||
console.log(
|
||||
'Selected new value from dropdown',
|
||||
htmlId,
|
||||
':',
|
||||
selected,
|
||||
'->',
|
||||
event.target.value
|
||||
);
|
||||
changed(event.target.value);
|
||||
}
|
||||
}
|
||||
|
||||
return html`
|
||||
<select
|
||||
id="${htmlId}"
|
||||
class="appearance-none cursor-pointer border rounded bg-grey-10 hover:border-blue-50 focus:border-blue-50 pl-1 pr-8 py-1 my-1 h-8 dark:bg-grey-80"
|
||||
class="appearance-none cursor-pointer border-default rounded-default bg-grey-10 hover:border-primary focus:border-primary pl-1 pr-8 py-1 my-1 h-8 dark:bg-grey-80"
|
||||
data-selected="${selected}"
|
||||
onchange="${choose}"
|
||||
>
|
||||
${options.map(
|
||||
i =>
|
||||
value =>
|
||||
html`
|
||||
<option value="${i}" ${i === selected ? 'selected' : ''}
|
||||
>${translate(i)}</option
|
||||
>
|
||||
<option value="${value}" ${value == selected ? 'selected' : ''}>
|
||||
${translate(value)}
|
||||
</option>
|
||||
`
|
||||
)}
|
||||
</select>
|
||||
`;
|
||||
|
||||
function choose(event) {
|
||||
const target = event.target;
|
||||
const value = +target.value;
|
||||
|
||||
if (x !== value) {
|
||||
x = value;
|
||||
changed(value);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
|
@ -18,7 +18,7 @@ module.exports = function(name, url) {
|
|||
<input
|
||||
type="text"
|
||||
id="share-url"
|
||||
class="w-full my-4 border rounded-lg leading-loose h-12 px-2 py-1 dark:bg-grey-80"
|
||||
class="w-full my-4 border-default rounded-lg leading-loose h-12 px-2 py-1 dark:bg-grey-80"
|
||||
value="${url}"
|
||||
readonly="true"
|
||||
/>
|
||||
|
@ -30,7 +30,7 @@ module.exports = function(name, url) {
|
|||
${state.translate('shareLinkButton')}
|
||||
</button>
|
||||
<button
|
||||
class="link-blue my-4 font-medium cursor-pointer focus:outline"
|
||||
class="link-primary my-4 font-medium cursor-pointer focus:outline"
|
||||
onclick="${close}"
|
||||
title="${state.translate('okButton')}"
|
||||
>
|
||||
|
|
|
@ -35,7 +35,7 @@ module.exports = function() {
|
|||
<input
|
||||
id="email-input"
|
||||
type="email"
|
||||
class="hidden border rounded-lg w-full px-2 py-1 h-12 mb-3 text-lg text-grey-70 leading-loose dark:bg-grey-80 dark:text-white"
|
||||
class="hidden border-default rounded-lg w-full px-2 py-1 h-12 mb-3 text-lg text-grey-70 leading-loose dark:bg-grey-80 dark:text-white"
|
||||
placeholder=${state.translate('emailPlaceholder')}
|
||||
/>
|
||||
<input
|
||||
|
@ -50,7 +50,7 @@ module.exports = function() {
|
|||
? ''
|
||||
: html`
|
||||
<button
|
||||
class="my-3 link-blue font-medium"
|
||||
class="my-3 link-primary font-medium"
|
||||
title="${state.translate('deletePopupCancel')}"
|
||||
onclick=${cancel}
|
||||
>
|
||||
|
|
|
@ -30,7 +30,7 @@ module.exports = function() {
|
|||
Give feedback
|
||||
</a>
|
||||
<button
|
||||
class="link-blue font-medium cursor-pointer focus:outline"
|
||||
class="link-primary font-medium cursor-pointer focus:outline"
|
||||
onclick="${close}"
|
||||
title="Skip"
|
||||
>
|
||||
|
|
|
@ -10,8 +10,8 @@ module.exports = function(state, emit) {
|
|||
strings = unsupportedStrings(state);
|
||||
why = html`
|
||||
<a
|
||||
class="text-blue"
|
||||
href="https://github.com/mozilla/send/blob/master/docs/faq.md#why-is-my-browser-not-supported"
|
||||
class="text-primary"
|
||||
href="https://github.com/timvisee/send/blob/master/docs/faq.md#why-is-my-browser-not-supported"
|
||||
>
|
||||
${state.translate('notSupportedLink')}
|
||||
</a>
|
||||
|
@ -27,7 +27,7 @@ module.exports = function(state, emit) {
|
|||
<main class="main">
|
||||
${state.modal && modal(state, emit)}
|
||||
<section
|
||||
class="flex flex-col items-center justify-center text-center bg-white m-6 px-6 py-8 border border-grey-30 md:border-none md:px-12 md:py-16 shadow w-full md:h-full dark:bg-grey-90"
|
||||
class="flex flex-col items-center justify-center text-center bg-white m-6 px-6 py-8 border-default border-grey-30 md:border-none md:px-12 md:py-16 shadow-default w-full md:h-full dark:bg-grey-90"
|
||||
>
|
||||
<h1 class="text-3xl font-bold">${strings.header}</h1>
|
||||
<p class="mt-4 mb-8 max-w-md leading-normal">${strings.description}</p>
|
||||
|
|
|
@ -29,7 +29,7 @@ class File {
|
|||
const v = new DataView(h);
|
||||
v.setUint32(0, 0x04034b50, true); // sig
|
||||
v.setUint16(4, 20, true); // version
|
||||
v.setUint16(6, 8, true); // bit flags (8 = use data descriptor)
|
||||
v.setUint16(6, 0x808, true); // bit flags (use data descriptor(8) + utf8-encoded(8 << 8))
|
||||
v.setUint16(8, 0, true); // compression
|
||||
v.setUint16(10, this.dateTime.time, true); // modified time
|
||||
v.setUint16(12, this.dateTime.date, true); // modified date
|
||||
|
@ -60,7 +60,7 @@ class File {
|
|||
v.setUint32(0, 0x02014b50, true); // sig
|
||||
v.setUint16(4, 20, true); // version made
|
||||
v.setUint16(6, 20, true); // version required
|
||||
v.setUint16(8, 8, true); // bit flags (8 = use data descriptor)
|
||||
v.setUint16(8, 0x808, true); // bit flags (use data descriptor(8) + utf8-encoded(8 << 8))
|
||||
v.setUint16(10, 0, true); // compression
|
||||
v.setUint16(12, this.dateTime.time, true); // modified time
|
||||
v.setUint16(14, this.dateTime.date, true); // modified date
|
||||
|
|
|
@ -1,26 +1,24 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1">
|
||||
<!--
|
||||
This would be the elegant way, but chrome cannot handle masks in external SVGs.
|
||||
See https://bugs.chromium.org/p/chromium/issues/detail?id=109212
|
||||
-->
|
||||
<!--
|
||||
<symbol id="icon" viewBox="0 0 26 32">
|
||||
<defs>
|
||||
<path d="M21.848475,31.6653183 L2.89005487,31.6653183 C1.32717435,31.6653183 0.104050469,30.3742431 0.104050469,28.8793139 L0.104050469,2.7860044 C0.104050469,1.22312388 1.39512568,0 2.89005487,0 L14.7135857,0 C15.4610503,0 16.2085149,0.271805307 16.6841742,0.815415921 L23.8190635,7.95030523 C24.3626741,8.49391584 24.6344794,9.17342911 24.6344794,9.9208937 L24.6344794,28.8793139 C24.6344794,30.3742431 23.4113555,31.6653183 21.848475,31.6653183 Z" id="path-1"></path>
|
||||
</defs>
|
||||
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g id="Send_Ready_to_Send-Spec" transform="translate(-277.000000, -206.000000)">
|
||||
<g id="List-Item-1" transform="translate(253.000000, 192.000000)">
|
||||
<g id="Locked-File-Icon" transform="translate(24.000000, 14.000000)">
|
||||
<g id="File-Icon">
|
||||
<mask id="mask-2" fill="white">
|
||||
<use xlink:href="#path-1"></use>
|
||||
</mask>
|
||||
<use id="Mask" fill="#45a1ff" xlink:href="#path-1"></use>
|
||||
<path d="M24.3031318,10.6474633 L16.7826187,10.6474633 C15.2742552,10.6474633 14.051485,9.42469306 14.051485,7.91632957 L14.051485,0.395816478" id="Path" stroke="currentColor" stroke-width="2" stroke-linecap="round" mask="url(#mask-2)"></path>
|
||||
<path d="M7.2830232,8.93885547 L8.91906464,8.93885547" id="Path" stroke="currentColor" stroke-width="2" stroke-linecap="round" mask="url(#mask-2)"></path>
|
||||
<path d="M7.2830232,15.7798836 L17.5412669,15.7798836" id="Path" stroke="currentColor" stroke-width="2" stroke-linecap="round" mask="url(#mask-2)"></path>
|
||||
<path d="M7.2830232,22.6209117 L17.5412669,22.6209117" id="Path" stroke="currentColor" stroke-width="2" stroke-linecap="round" mask="url(#mask-2)"></path>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g id="Fire-Icon">
|
||||
<mask id="mask">
|
||||
<rect x="0" y="0" width="100" height="100" fill="white"/>
|
||||
<path d="M7.2830232,8.93885547 L8.91906464,8.93885547" stroke="black" stroke-width="2" stroke-linecap="round"/>
|
||||
<path d="M7.2830232,15.7798836 L17.5412669,15.7798836" stroke="black" stroke-width="2" stroke-linecap="round"/>
|
||||
<path d="M7.2830232,22.6209117 L17.5412669,22.6209117" stroke="black" stroke-width="2" stroke-linecap="round"/>
|
||||
<path d="M24.3031318,10.6474633 L16.7826187,10.6474633 C15.2742552,10.6474633 14.051485,9.42469306 14.051485,7.91632957 L14.051485,0.395816478" id="Path" stroke-linecap="square" stroke="black" stroke-width="2" fill="none"/>
|
||||
</mask>
|
||||
<path d="M21.848475,31.6653183 L2.89005487,31.6653183 C1.32717435,31.6653183 0.104050469,30.3742431 0.104050469,28.8793139 L0.104050469,2.7860044 C0.104050469,1.22312388 1.39512568,0 2.89005487,0 L14.7135857,0 C15.4610503,0 16.2085149,0.271805307 16.6841742,0.815415921 L23.8190635,7.95030523 C24.3626741,8.49391584 24.6344794,9.17342911 24.6344794,9.9208937 L24.6344794,28.8793139 C24.6344794,30.3742431 23.4113555,31.6653183 21.848475,31.6653183 Z" id="path-1" fill="currentColor" mask="url(#mask)" />
|
||||
</g>
|
||||
</g>
|
||||
</symbol>
|
||||
-->
|
||||
<symbol id="icon" viewBox="0 0 26 32">
|
||||
<path d="m 15 0 c 0.7 0.1 1.233 0.331 1.7 0.8 l 7.1 7.1 c 0.5 0.5 0.8 1.1 0.8 1.8 h -7.9 c -1 0 -1.7 -0.8 -1.7 -1.7 V 0.4 z M 24.65 11.647 v 17.23 c 0 1.5 -1.2 2.8 -2.8 2.8 h -19 c -1.234 -0.017 -2.694 -1.094 -2.7 -2.8 V 2.8 C 0.1 1.2 1.4 0 2.9 0 h 10.15 v 7.5 c -0.135 2.533 1.669 4.119 3.7 4.15 h 7.9 z M 6.3 8.9 c 0 0.6 0.4 1 1 1 h 1.6 c 0.5 0 1 -0.4 1 -1 c 0 -0.5 -0.5 -1 -1 -1 H 7.3 C 6.7 7.9 6.3 8.4 6.3 8.9 z M 18.5 22.6 c 0 -0.5 -0.4 -1 -1 -1 H 7.3 c -0.6 0 -1 0.5 -1 1 s 0.4 1 1 1 h 10.3 C 18.1 23.6 18.5 23.2 18.5 22.6 z M 18.5 15.8 c 0 -0.6 -0.4 -1 -1 -1 H 7.3 c -0.6 0 -1 0.4 -1 1 s 0.4 1 1 1 h 10.3 C 18.1 16.8 18.5 16.3 18.5 15.8 z" fill="currentColor" />
|
||||
</symbol>
|
||||
</svg>
|
Before Width: | Height: | Size: 2.1 KiB After Width: | Height: | Size: 2.4 KiB |
|
@ -253,7 +253,7 @@
|
|||
<path
|
||||
id="cloud-check"
|
||||
d="m 240.12048,110.31165 a 39.582604,39.582604 0 0 1 -10.23938,26.55813 33.935974,33.935974 0 0 1 -20.28366,10.27635 39.949372,39.949372 0 0 0 0.80301,-16.48404 16.944722,16.944722 0 0 0 7.65405,-4.47239 23.665469,23.665469 0 0 0 6.1311,-15.87805 26.589359,26.589359 0 0 0 -26.55812,-26.558123 c -0.87568,0 -5.3742,0.56186 -8.11085,0.91445 a 2.6398778,2.6398778 0 0 1 -2.83769,-1.77451 l -1.08155,-3.155106 A 32.772357,32.772357 0 0 0 158.37085,57.356128 31.757517,31.757517 0 0 0 124.14114,96.347867 l 1.13419,4.851433 a 2.6558127,2.6558127 0 0 1 -1.8179,3.14687 l -4.77313,1.44232 a 15.788381,15.788381 0 0 0 -11.35445,15.14642 10.699791,10.699791 0 0 0 2.19933,6.52543 12.871343,12.871343 0 0 0 9.50281,4.09782 h 7.25749 a 40.137987,40.137987 0 0 0 1.02966,15.93487 h -8.28715 A 28.433555,28.433555 0 0 1 96.535402,136.68302 26.739094,26.739094 0 0 1 91.39497,120.93491 31.54075,31.54075 0 0 1 107.53217,93.318597 c -0.13492,-1.42644 -0.20238,-2.84772 -0.20238,-4.25345 a 47.786197,47.786197 0 0 1 90.59848,-21.246498 42.542454,42.542454 0 0 1 42.19216,42.493001 z m -39.83719,26.55813 a 31.869752,31.869752 0 1 1 -31.86975,-31.86975 31.869752,31.869752 0 0 1 31.86975,31.86975 z m -15.15067,-11.86394 -2.71673,-2.01199 a 1.5763311,1.5763311 0 0 0 -2.00127,0.14182 l -16.73162,15.72576 -9.38033,-4.48657 a 1.7144864,1.7144864 0 0 0 -2.00221,0.5062 l -2.30275,2.94497 a 1.576756,1.576756 0 0 0 0.14182,2.00158 l 11.86234,11.8768 a 4.410933,4.410933 0 0 0 6.89242,-0.53116 l 16.51629,-24.2992 a 1.346922,1.346922 0 0 0 -0.27833,-1.86831 z"
|
||||
style="fill:#45a1ff;fill-opacity:1;stroke-width:5.31163" />
|
||||
style="fill-opacity:1;stroke-width:5.31163" fill="currentColor" />
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
|
|
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 21 KiB |
|
@ -5,8 +5,6 @@
|
|||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="356px"
|
||||
height="210px"
|
||||
viewBox="0 0 356 210"
|
||||
version="1.1"
|
||||
id="svg114">
|
||||
|
@ -256,5 +254,5 @@
|
|||
<path
|
||||
id="cloud-alert"
|
||||
d="m 254.54362,110.61709 a 41.858804,41.858804 0 0 1 -10.8282,28.08534 35.887465,35.887465 0 0 1 -21.45007,10.8673 42.246663,42.246663 0 0 0 0.84918,-17.43196 17.919131,17.919131 0 0 0 8.09421,-4.72957 25.026353,25.026353 0 0 0 6.48366,-16.79111 28.118382,28.118382 0 0 0 -28.08535,-28.085359 c -0.92602,0 -5.68324,0.594174 -8.57727,0.967035 a 2.7916842,2.7916842 0 0 1 -3.0008,-1.876551 l -1.14369,-3.336147 A 34.656989,34.656989 0 0 0 168.09302,54.616462 33.58368,33.58368 0 0 0 131.89488,95.850311 l 1.19942,5.130409 a 2.8085354,2.8085354 0 0 1 -1.92244,3.32783 l -5.04762,1.52526 a 16.696293,16.696293 0 0 0 -12.00739,16.01742 11.315083,11.315083 0 0 0 2.32581,6.90069 13.611511,13.611511 0 0 0 10.04928,4.33345 h 7.67482 a 42.446125,42.446125 0 0 0 1.08887,16.85122 h -8.76369 A 30.06863,30.06863 0 0 1 102.70166,138.50495 28.276728,28.276728 0 0 1 97.265633,121.85123 33.354503,33.354503 0 0 1 114.33081,92.64684 c -0.14268,-1.508465 -0.21401,-3.01148 -0.21401,-4.498037 a 50.534146,50.534146 0 0 1 95.80834,-22.468284 44.988861,44.988861 0 0 1 44.61843,44.936571 z m -42.12804,28.08534 a 33.702425,33.702425 0 1 1 -33.70241,-33.70242 33.702425,33.702425 0 0 1 33.70241,33.70242 z m -28.08535,14.04269 a 2.8085354,2.8085354 0 0 0 -2.80853,-2.80853 h -5.61707 a 2.8085354,2.8085354 0 0 0 -2.80853,2.80853 v 5.61706 a 2.8085354,2.8085354 0 0 0 2.80853,2.80855 h 5.61707 a 2.8085354,2.8085354 0 0 0 2.80853,-2.80855 z m 0,-33.70243 a 2.8166239,2.8166239 0 0 0 -2.80853,-2.80853 h -5.61707 a 2.8166239,2.8166239 0 0 0 -2.80853,2.80853 v 11.23415 a 22.144684,22.144684 0 0 0 0.83447,5.49022 l 2.24351,7.21126 a 2.0333796,2.0333796 0 0 0 1.82117,1.34118 h 1.43583 a 2.0333796,2.0333796 0 0 0 1.82118,-1.34118 l 2.2435,-7.21126 a 22.144684,22.144684 0 0 0 0.83447,-5.49022 z"
|
||||
style="stroke-width:5.61707;fill:#45a1ff;fill-opacity:1" />
|
||||
style="stroke-width:5.61707;fill:currentColor;fill-opacity:1" />
|
||||
</svg>
|
||||
|
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 24 KiB |
BIN
assets/icon-64x64.png
Normal file
After Width: | Height: | Size: 1.9 KiB |
|
@ -5,8 +5,6 @@
|
|||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="348px"
|
||||
height="210px"
|
||||
viewBox="0 0 348 210"
|
||||
version="1.1"
|
||||
id="svg124">
|
||||
|
@ -272,5 +270,5 @@
|
|||
<path
|
||||
id="cloud-off"
|
||||
d="M 232.66609,50.155807 A 8.2118103,8.2118103 0 0 0 221.05286,38.54253 l -24.58739,24.587284 a 49.087021,49.087021 0 0 0 -82.69072,35.970548 c 0,1.448998 0.0696,2.914018 0.20861,4.384368 a 32.511588,32.511588 0 0 0 -16.633903,28.46635 27.562135,27.562135 0 0 0 5.298643,16.23286 22.799931,22.799931 0 0 0 4.2838,4.4793 l -7.176402,7.17673 a 8.2118072,8.2118072 0 1 0 11.613272,11.61322 z M 118.72657,140.86955 a 8.5946237,8.5946237 0 0 1 -2.68479,-2.19217 11.029135,11.029135 0 0 1 -2.26703,-6.7263 16.274355,16.274355 0 0 1 11.70415,-15.61263 l 4.92005,-1.48644 a 2.738162,2.738162 0 0 0 1.8737,-3.24418 l -1.16893,-5.00059 a 32.735029,32.735029 0 0 1 35.28314,-40.191861 31.860268,31.860268 0 0 1 18.36694,8.426592 z m 131.92616,-19.86871 a 40.800975,40.800975 0 0 1 -10.55455,27.3756 35.587128,35.587128 0 0 1 -25.2904,10.95024 h -71.10166 l 16.42536,-16.42536 h 54.6763 a 19.17354,19.17354 0 0 0 13.09966,-5.53393 24.393903,24.393903 0 0 0 6.31993,-16.36655 27.319807,27.319807 0 0 0 -25.05414,-27.140158 l 13.61114,-13.61313 a 43.833807,43.833807 0 0 1 27.86836,40.753288 z"
|
||||
style="fill:#45a1ff;fill-opacity:1;stroke-width:5.47512" />
|
||||
style="fill:currentColor;fill-opacity:1;stroke-width:5.47512" />
|
||||
</svg>
|
||||
|
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 24 KiB |
65
assets/thunderbird-icon.svg
Normal file
|
@ -0,0 +1,65 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 750 750">
|
||||
<path fill="url(#a)" d="m 314.805,154.949 h 0.06 c 22.04,-77.0499 118.08,-114.6599 215.95,-114.6599 67.63,0 128.341,21.41 169.961,55.4 -24.838,1.1712 -49.362,6.0449 -72.76,14.4599 33.63,12.5 62.519,31.73 83.929,55.52 -16.153,-2.78 -32.532,-4.019 -48.919,-3.7 40.276,58.343 61.794,127.585 61.68,198.48 0,193.3 -156.701,350 -350.001,350 -190.32,0 -349.9999,-159.35 -349.9999,-350 0,-30.11 4,-61.2 11.77,-90.36 2.04,-6.12 4.88,-11.99 8.65,-14.14 4.72,-2.69 9.02,5.33 9.71,7.94 5.1177,19.17 12.0039,37.823 20.57,55.72 -0.75,-39.96 16.32,-76.36 39.7999,-107.84 15.66,-20.99 30.18,-40.44 36.88,-96.57 0.45,-3.77 4.02,-6.48 7.63,-5.3 50.96,16.68 78.2,101.54 73.97,172.5 28.15,4.03 28.02,-25.38 28.02,-25.38 -9,-27.66 -3,-79.07 53,-102.07 z"/>
|
||||
<path fill="url(#b)" d="m 713.254,271.32 c 8.51,192.5 -149.74,358.47 -342.71,358.47 -180.65,0 -328.6792,-139.63 -342.0692,-316.85 -2.3889,16.596 -3.6452,33.334 -3.76,50.1 1.37,189.62 160.6592,347.41 349.9992,347.41 193.3,0 350,-156.7 350,-350 0,-30.8 -3.99,-60.67 -11.46,-89.13 z" opacity="0.9"/>
|
||||
<path fill="url(#c)" d="m 366.175,180.459 c -3.77,-6.67 -21.18,-16.54 -28.79,-18.26 28.8,-92.2499 175.521,-120.5699 265.321,-104.2499 37.37,6.8 83.91,27.16 98.07,37.74 -41.62,-33.99 -102.341,-55.4 -169.961,-55.4 -97.87,0 -193.91,37.61 -215.95,114.6599 h -0.16 c -56,23 -62,74.43 -53,102.08 8.64,-32.99 49.75,-73.53 104.47,-76.57 z" style="mix-blend-mode:screen"/>
|
||||
<path fill="url(#d)" d="m 467.885,116.748 c -78.62,15.47 -104.31,20.53 -130.62,45.54 29.55,-78.2297 104.98,-94.0897 194.86,-58.43 -21.391,4.41 -42.805,8.71 -64.24,12.9 z"/>
|
||||
<path fill="url(#e)" d="m 43.7049,259.38 c -21.48,87.94 -4.88,191.3 92.6991,278.04 -29.05,-31.77 -64.5191,-149.07 13.75,-232.87 5.27,-5.65 14.34,-1.5 14.63,6.22 6.45,174.16 146.98,280.53 309,260.64 -50.2,-2.82 -216.22,-60.97 -92.72,-83.97 64.55,-12.03 165.76,-30.88 165.76,-121.7 0,-147.22 -113.83,-190.26 -182.86,-183.86 -47.24,4.38 -89.29,34.36 -102.23,75.13 4.97,16.07 -14.84,27.32 -28.05,25.43 4.24,-70.95 -23,-155.86 -73.97,-172.54 -3.6,-1.18 -7.18,1.53 -7.63,5.3 -6.7,56.13 -21.22,75.58 -36.87,96.57 -23.4891,31.49 -40.5591,67.88 -39.8091,107.84 -8.5659,-17.897 -15.4521,-36.55 -20.57,-55.72 -0.57,-2.16 -3.7,-8.19 -7.48,-8.47 -2.05,-0.15 -3.14,1.85 -3.65,3.96 z"/>
|
||||
<path fill="url(#f)" d="m 337.695,496.349 c 95.04,77.17 286.17,19.31 286.17,-168.32 -77.16,116.96 -175.44,197.64 -286.16,168.32 z" style="mix-blend-mode:screen"/>
|
||||
<path fill="url(#g)" d="m 150.155,304.55 c 0.864,-0.967 1.953,-1.706 3.17,-2.153 1.217,-0.446 2.526,-0.586 3.81,-0.407 -70.0595,85.45 -13.549,235.52 25.231,272.39 2.17,6.14 -36.791,-25.79 -42.161,-33.26 -29.5,-25.03 -71.7795,-149.06 9.95,-236.57 z" style="mix-blend-mode:screen"/>
|
||||
<path fill="url(#h)" d="m 374.705,503.419 c 95.06,0 172.13,-62.879 172.13,-140.449 0,-77.57 -77.07,-140.45 -172.13,-140.45 -81.1,0 -172.17,52.76 -172.13,142.5 0.04,138.67 146.54,218.45 271.42,206.37 -9.38,-1.09 -67.9,-4.2 -107.45,-48.94 -3.57,-4.03 -9.76,-11.07 -6.95,-15.64 2.8,-4.57 10.52,-3.4 15.1,-3.4 z"/>
|
||||
<path fill="#fff" d="m 528.085,299.15 -136.04,130.18 c -12.07,8.58 -24.92,9.2 -37.6,1.42 L 221.015,299.63 c 3.807,-6.118 8.087,-11.929 12.8,-17.38 l 14,13.1 c 35.04,32.84 63.37,59.37 103.3,93.45 18.02,15.38 23.62,15.08 41.3,0 45.68,-39 79.09,-68.5 122.52,-107.29 4.841,5.526 9.235,11.428 13.14,17.65 z" opacity="0.6"/>
|
||||
<mask id="i" width="345" height="276" x="202" y="297" maskUnits="userSpaceOnUse">
|
||||
<path fill="#fff" d="m 546.835,362.969 c 0,77.57 -77.07,140.45 -172.13,140.45 -4.59,0 -12.3,-1.18 -15.11,3.4 -2.81,4.56 3.38,11.6 6.95,15.63 37.16,42.04 91.07,47.33 105.22,48.72 l 2.23,0.22 c -124.88,12.08 -271.38,-67.7 -271.42,-206.37 -0.131,-23.089 6.295,-45.74 18.53,-65.32 l 134.08,121.78 c 9.54,8.66 25.61,8.66 35.15,0 l 136.62,-124.09 c 12.7,19.58 19.88,41.9 19.88,65.58 z"/>
|
||||
</mask>
|
||||
<g mask="url(#i)">
|
||||
<path fill="url(#j)" d="m 162.705,200.979 h 435.07 v 394.47 h -435.07 z" opacity="0.7"/>
|
||||
<g filter="url(#k)">
|
||||
<path fill="#458fcd" fill-rule="evenodd" d="m 334.745,422.298 c -25.35,-26.38 -101.27,-114.03 -101.27,-114.03 l 5.87,0.26 118.86,88.77 c 8.9,6.47 21.6,6.4 30.42,-0.15 l 116.55,-88.5 6.18,-0.49 c 0,0 -73.44,85.57 -101.57,113.76 -28.13,28.19 -49.69,26.76 -75.04,0.38 z" clip-rule="evenodd"/>
|
||||
</g>
|
||||
</g>
|
||||
<path fill="#fff" d="m 398.855,152.258 c 18.42,-5.8 16.8,-24.03 16.8,-24.03 0,0 -9.21,-10.85 -27.46,-4.84 -17.08,5.63 -19.73,17.8 -19.73,17.8 0,0 9.33,17.7 30.39,11.07 z"/>
|
||||
<defs>
|
||||
<linearGradient id="a" x1="146.185" x2="639.58502" y1="155.649" y2="615.86902" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#1B91F3"/>
|
||||
<stop offset="1" stop-color="#0B68CB"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="d" x1="283.47501" x2="461.01501" y1="273.45801" y2="75.818298" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#0F5DB0"/>
|
||||
<stop offset="1" stop-color="#0F5DB0" stop-opacity="0"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="f" x1="594.625" x2="512.07501" y1="416.55899" y2="619.24902" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#E247C4" stop-opacity="0"/>
|
||||
<stop offset="1" stop-color="#E247C4" stop-opacity="0.64"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="g" x1="82.7155" x2="155.44501" y1="234.78999" y2="527.51001" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0.1" stop-color="#EF3ACC"/>
|
||||
<stop offset="1" stop-color="#EF3ACC" stop-opacity="0"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="h" x1="374.70499" x2="374.70499" y1="273.95001" y2="569.94897" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#fff"/>
|
||||
<stop offset="0.91" stop-color="#BEE1FE"/>
|
||||
<stop offset="1" stop-color="#96CEFD"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="j" x1="380.245" x2="380.245" y1="441.44901" y2="565.44897" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#BCE0FD"/>
|
||||
<stop offset="1" stop-color="#88CCFC"/>
|
||||
</linearGradient>
|
||||
<radialGradient id="b" cx="0" cy="0" r="1" gradientTransform="matrix(117.92992,271.44954,-260.03887,112.97261,145.274,314)" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0.53" stop-color="#0B4186" stop-opacity="0"/>
|
||||
<stop offset="1" stop-color="#0B4186" stop-opacity="0.45"/>
|
||||
</radialGradient>
|
||||
<radialGradient id="c" cx="0" cy="0" r="1" gradientTransform="matrix(-38.950414,-49.859637,82.506525,-64.454205,383.705,192.449)" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#EF3ACC" stop-opacity="0"/>
|
||||
<stop offset="1" stop-color="#EF3ACC" stop-opacity="0.64"/>
|
||||
</radialGradient>
|
||||
<radialGradient id="e" cx="0" cy="0" r="1" gradientTransform="matrix(200.50032,-415.91019,513.77136,247.67684,242.704,551.15)" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0.02" stop-color="#094188"/>
|
||||
<stop offset="0.97" stop-color="#0B4186" stop-opacity="0"/>
|
||||
</radialGradient>
|
||||
<filter id="k" width="341.88" height="198.416" x="201.47501" y="276.15799" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
|
||||
<feGaussianBlur result="effect1_foregroundBlur_106_1002" stdDeviation="16"/>
|
||||
</filter>
|
||||
</defs>
|
||||
</svg>
|
After Width: | Height: | Size: 7.1 KiB |
236
docs/AWS.md
Normal file
|
@ -0,0 +1,236 @@
|
|||
# Deployment to AWS
|
||||
|
||||
This document describes how to do a deployment of Send in AWS
|
||||
|
||||
## AWS requirements
|
||||
|
||||
### Security groups (2)
|
||||
|
||||
* ALB:
|
||||
- inbound: allow traffic from anywhere on port 80 and 443
|
||||
- ountbound: allow traffic to the instance security group on port `8080`
|
||||
|
||||
* Instance:
|
||||
- inbound: allow SSH from your public IP or a bastion (changing the default SSH port is a good idea)
|
||||
- inbound: allow traffic from the ALB security group on port `8080`
|
||||
- ountbound: allow all traffic to anywhere
|
||||
|
||||
### Resources
|
||||
|
||||
* An S3 bucket (block all public access)
|
||||
|
||||
* A private EC2 instance running Ubuntu `20.04` (you can use the [Amazon EC2 AMI Locator](https://cloud-images.ubuntu.com/locator/ec2/) to find the latest)
|
||||
|
||||
Attach an IAM role to the instance with the following inline policy:
|
||||
|
||||
```json
|
||||
{
|
||||
"Version": "2012-10-17",
|
||||
"Statement": [
|
||||
{
|
||||
"Action": [
|
||||
"s3:ListAllMyBuckets"
|
||||
],
|
||||
"Resource": [
|
||||
"*"
|
||||
],
|
||||
"Effect": "Allow"
|
||||
},
|
||||
{
|
||||
"Action": [
|
||||
"s3:ListBucket",
|
||||
"s3:GetBucketLocation",
|
||||
"s3:ListBucketMultipartUploads"
|
||||
],
|
||||
"Resource": [
|
||||
"arn:aws:s3:::<s3_bucket_name>"
|
||||
],
|
||||
"Effect": "Allow"
|
||||
},
|
||||
{
|
||||
"Action": [
|
||||
"s3:GetObject",
|
||||
"s3:GetObjectVersion",
|
||||
"s3:ListMultipartUploadParts",
|
||||
"s3:PutObject",
|
||||
"s3:AbortMultipartUpload",
|
||||
"s3:DeleteObject",
|
||||
"s3:DeleteObjectVersion"
|
||||
],
|
||||
"Resource": [
|
||||
"arn:aws:s3:::<s3_bucket_name>/*"
|
||||
],
|
||||
"Effect": "Allow"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
* A public ALB:
|
||||
|
||||
- Create a target group with the instance registered (HTTP on port `8080` and path `/`)
|
||||
- Configure HTTP (port 80) to redirect to HTTPS (port 443)
|
||||
- HTTPS (port 443) using the latest security policy and an ACM certificate like `send.mydomain.com`
|
||||
|
||||
* A Route53 public record, alias from `send.mydomain.com` to the ALB
|
||||
|
||||
## Software requirements
|
||||
|
||||
* Git
|
||||
* NodeJS `15.x` LTS
|
||||
* Local Redis server
|
||||
|
||||
### Prerequisite packages
|
||||
|
||||
```bash
|
||||
sudo apt update
|
||||
sudo apt install -y apt-transport-https ca-certificates curl software-properties-common
|
||||
```
|
||||
|
||||
### Add repositories
|
||||
|
||||
* NodeJS `15.x` LTS (checkout [package.json](../package.json)):
|
||||
|
||||
```bash
|
||||
curl -fsSL https://deb.nodesource.com/gpgkey/nodesource.gpg.key | sudo apt-key add -
|
||||
echo 'deb [arch=amd64] https://deb.nodesource.com/node_15.x focal main' | sudo tee /etc/apt/sources.list.d/nodejs.list
|
||||
```
|
||||
|
||||
* Git (latest)
|
||||
|
||||
```bash
|
||||
sudo add-apt-repository ppa:git-core/ppa
|
||||
```
|
||||
|
||||
* Redis (latest)
|
||||
|
||||
```bash
|
||||
sudo add-apt-repository ppa:redislabs/redis
|
||||
```
|
||||
|
||||
### Install required packages
|
||||
|
||||
```bash
|
||||
sudo apt update
|
||||
sudo apt install git nodejs redis-server telnet
|
||||
```
|
||||
|
||||
### Redis server
|
||||
|
||||
#### Password (optional)
|
||||
|
||||
Generate a strong password:
|
||||
|
||||
```bash
|
||||
makepasswd --chars=100
|
||||
```
|
||||
|
||||
Edit Redis configuration file `/etc/redis/redis.conf`:
|
||||
|
||||
```bash
|
||||
requirepass <redis_password>
|
||||
```
|
||||
|
||||
_Note: documentation on securing Redis https://redis.io/topics/security_
|
||||
|
||||
#### Systemd
|
||||
|
||||
Enable and (re)start the Redis server service:
|
||||
|
||||
```bash
|
||||
sudo systemctl enable redis-server
|
||||
sudo systemctl restart redis-server
|
||||
sudo systemctl status redis-server
|
||||
```
|
||||
|
||||
## Website directory
|
||||
|
||||
Setup a directory for the data
|
||||
|
||||
```
|
||||
sudo mkdir -pv /var/www/send
|
||||
sudo chown www-data:www-data /var/www/send
|
||||
sudo 750 /var/www/send
|
||||
```
|
||||
|
||||
### NodeJS
|
||||
|
||||
Update npm:
|
||||
|
||||
```bash
|
||||
sudo npm install -g npm
|
||||
```
|
||||
|
||||
Checkout current NodeJS and npm versions:
|
||||
|
||||
```bash
|
||||
node --version
|
||||
npm --version
|
||||
```
|
||||
|
||||
Clone repository, install JavaScript packages and compiles the assets:
|
||||
|
||||
```bash
|
||||
sudo su -l www-data -s /bin/bash
|
||||
cd /var/www/send
|
||||
git clone https://gitlab.com/timvisee/send.git .
|
||||
npm install
|
||||
npm run build
|
||||
exit
|
||||
```
|
||||
|
||||
Create the file `/var/www/send/.env` used by Systemd with your environment variables
|
||||
(checkout [config.js](../server/config.js) for more configuration environment variables):
|
||||
|
||||
```
|
||||
BASE_URL='https://send.mydomain.com'
|
||||
NODE_ENV='production'
|
||||
PORT='8080'
|
||||
REDIS_PASSWORD='<redis_password>'
|
||||
S3_BUCKET='<s3_bucket_name>'
|
||||
```
|
||||
|
||||
Lower files and folders permissions to user and group `www-data`:
|
||||
|
||||
```
|
||||
sudo find /var/www/send -type d -exec chmod 750 {} \;
|
||||
sudo find /var/www/send -type f -exec chmod 640 {} \;
|
||||
sudo find -L /var/www/send/node_modules/.bin/ -exec chmod 750 {} \;
|
||||
```
|
||||
|
||||
### Systemd
|
||||
|
||||
Create the file `/etc/systemd/system/send.service` with `root` user and `644` mode:
|
||||
|
||||
```
|
||||
[Unit]
|
||||
Description=Send
|
||||
After=network.target
|
||||
Requires=redis-server.service
|
||||
Documentation=https://gitlab.com/timvisee/send
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
ExecStart=/usr/bin/npm run prod
|
||||
EnvironmentFile=/var/www/send/.env
|
||||
WorkingDirectory=/var/www/send
|
||||
User=www-data
|
||||
Group=www-data
|
||||
Restart=on-failure
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
```
|
||||
|
||||
_Note: could be better tuner to secure the service by restricting system permissions,
|
||||
check with `systemd-analyze security send`_
|
||||
|
||||
Enable and start the Send service, check logs:
|
||||
|
||||
```
|
||||
sudo systemctl daemon-reload
|
||||
sudo systemctl enable send
|
||||
sudo systemctl start send
|
||||
sudo systemctl status send
|
||||
journalctl -fu send
|
||||
```
|
|
@ -1,69 +1,96 @@
|
|||
## Requirements
|
||||
|
||||
This document describes how to do a full deployment of Send on your own Linux server. You will need:
|
||||
|
||||
* A working (and ideally somewhat recent) installation of NodeJS and NPM
|
||||
* GIT
|
||||
* An Apache webserver
|
||||
* A working (and ideally somewhat recent) installation of NodeJS and npm
|
||||
* Git
|
||||
* Apache webserver
|
||||
* Optionally telnet, to be able to quickly check your installation
|
||||
|
||||
For Debian/Ubuntu systems this probably just means something like this:
|
||||
For example in Debian/Ubuntu systems:
|
||||
|
||||
* apt install git apache2 nodejs npm telnet
|
||||
```bash
|
||||
sudo apt install git apache2 nodejs npm telnet
|
||||
```
|
||||
|
||||
## Building
|
||||
|
||||
* We assume an already configured virtual-host on your webserver with an existing empty htdocs folder
|
||||
* First, remove that htdocs folder - we will replace it with Send's version now
|
||||
* git clone https://github.com/mozilla/send.git htdocs
|
||||
* git clone https://github.com/timvisee/send.git htdocs
|
||||
* Make now sure you are NOT root but rather the user your webserver is serving files under (e.g. "su www-data" or whoever the owner of your htdocs folder is)
|
||||
* npm install
|
||||
* npm run build
|
||||
|
||||
## Running
|
||||
|
||||
To have a permanently running version of Send as a background process:
|
||||
|
||||
* Create a file "run.sh" with:
|
||||
```
|
||||
* Create a file `run.sh` with:
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
nohup su www-data -c "npm run prod" 2>/dev/null &
|
||||
```
|
||||
* chmod +x run.sh
|
||||
* ./run.sh
|
||||
|
||||
* Execute the script:
|
||||
|
||||
```bash
|
||||
chmod +x run.sh
|
||||
./run.sh
|
||||
```
|
||||
|
||||
Now the Send backend should be running on port 1443. You can check with:
|
||||
* telnet localhost 1443
|
||||
|
||||
```bash
|
||||
telnet localhost 1443
|
||||
```
|
||||
|
||||
## Reverse Proxy
|
||||
|
||||
Of course, we don't want to expose the service on port 1443. Instead we want our normal webserver to forward all requests to Send ("Reverse proxy").
|
||||
|
||||
# Apache webserver
|
||||
|
||||
* a2enmod proxy
|
||||
* a2enmod proxy_http
|
||||
* a2enmod proxy_wstunnel
|
||||
* a2enmod rewrite
|
||||
* Enable Apache required modules:
|
||||
|
||||
In your Apache virtual host configuration file, insert this:
|
||||
```bash
|
||||
sudo a2enmod headers
|
||||
sudo a2enmod proxy
|
||||
sudo a2enmod proxy_http
|
||||
sudo a2enmod proxy_wstunnel
|
||||
sudo a2enmod rewrite
|
||||
```
|
||||
|
||||
* Edit your Apache virtual host configuration file, insert this:
|
||||
|
||||
```
|
||||
# Enable rewrite engine
|
||||
RewriteEngine on
|
||||
# Enable rewrite engine
|
||||
RewriteEngine on
|
||||
|
||||
# Make sure the original domain name is forwarded to Send
|
||||
# Otherwise the generated URLs will be wrong
|
||||
ProxyPreserveHost on
|
||||
# Make sure the original domain name is forwarded to Send
|
||||
# Otherwise the generated URLs will be wrong
|
||||
ProxyPreserveHost on
|
||||
|
||||
# Make sure the generated URL is https://
|
||||
RequestHeader set X-Forwarded-Proto https
|
||||
# Make sure the generated URL is https://
|
||||
RequestHeader set X-Forwarded-Proto https
|
||||
|
||||
# If it's a normal file (e.g. PNG, CSS) just return it
|
||||
RewriteCond %{REQUEST_FILENAME} -f
|
||||
RewriteRule .* - [L]
|
||||
# If it's a normal file (e.g. PNG, CSS) just return it
|
||||
RewriteCond %{REQUEST_FILENAME} -f
|
||||
RewriteRule .* - [L]
|
||||
|
||||
# If it's a websocket connection, redirect it to a Send WS connection
|
||||
RewriteCond %{HTTP:Upgrade} =websocket [NC]
|
||||
RewriteRule /(.*) ws://127.0.0.1:1443/$1 [P,L]
|
||||
# If it's a websocket connection, redirect it to a Send WS connection
|
||||
RewriteCond %{HTTP:Upgrade} =websocket [NC]
|
||||
RewriteRule /(.*) ws://127.0.0.1:1443/$1 [P,L]
|
||||
|
||||
# Otherwise redirect it to a normal HTTP connection
|
||||
RewriteRule ^/(.*)$ http://127.0.0.1:1443/$1 [P,QSA]
|
||||
ProxyPassReverse "/" "http://127.0.0.1:1443"
|
||||
# Otherwise redirect it to a normal HTTP connection
|
||||
RewriteRule ^/(.*)$ http://127.0.0.1:1443/$1 [P,QSA]
|
||||
ProxyPassReverse "/" "http://127.0.0.1:1443"
|
||||
```
|
||||
|
||||
* Test configuration and restart Apache:
|
||||
|
||||
```bash
|
||||
sudo apache2ctl configtest
|
||||
sudo systemctl restart apache2
|
||||
```
|
||||
|
|
158
docs/docker.md
|
@ -1,45 +1,159 @@
|
|||
## Setup
|
||||
## Docker Quickstart
|
||||
|
||||
Use `registry.gitlab.com/timvisee/send:latest` from [`timvisee/send`'s registry](https://gitlab.com/timvisee/send/container_registry) for the latest Docker image.
|
||||
Use `registry.gitlab.com/timvisee/send:latest` from [`timvisee/send`'s Gitlab image registry](https://gitlab.com/timvisee/send/container_registry) for the latest Docker image.
|
||||
|
||||
```bash
|
||||
docker pull registry.gitlab.com/timvisee/send:latest
|
||||
|
||||
# example quickstart (point REDIS_HOST to an already-running redis server)
|
||||
docker run -v $PWD/uploads:/uploads -p 1443:1443 \
|
||||
-e 'DETECT_BASE_URL=true' \
|
||||
-e 'REDIS_HOST=localhost' \
|
||||
-e 'FILE_DIR=/uploads' \
|
||||
registry.gitlab.com/timvisee/send:latest
|
||||
```
|
||||
|
||||
Or run `docker build -t send:latest .` to create an image locally or `docker-compose up` to run a full testable stack. *We don't recommend using docker-compose for production.*
|
||||
Or clone this repo and run `docker build -t send:latest .` to build an image locally.
|
||||
|
||||
## Environment variables:
|
||||
*Note: for Docker Compose, see: https://github.com/timvisee/send-docker-compose*
|
||||
|
||||
| Name | Description
|
||||
## Environment Variables
|
||||
|
||||
All the available config options and their defaults can be found here: https://github.com/timvisee/send/blob/master/server/config.js
|
||||
|
||||
Config options should be set as unquoted environment variables. Boolean options should be `true`/`false`, time/duration should be integers (seconds), and filesize values should be integers (bytes).
|
||||
|
||||
Config options expecting array values (e.g. `EXPIRE_TIMES_SECONDS`, `DOWNLOAD_COUNTS`) should be in unquoted CSV format. UI dropdowns will default to the first value in the CSV, e.g. `DOWNLOAD_COUNTS=5,1,10,100` will show four dropdown options, with `5` selected by the default.
|
||||
|
||||
#### Server Configuration
|
||||
|
||||
| Name | Description |
|
||||
|------------------|-------------|
|
||||
| `BASE_URL` | The HTTPS URL where traffic will be served (e.g. `https://send.firefox.com`)
|
||||
| `PORT` | Port the server will listen on (defaults to 1443).
|
||||
| `NODE_ENV` | `"production"`
|
||||
| `FILE_DIR` | Uploads directory for local storage
|
||||
| `S3_BUCKET` | The S3 bucket name.
|
||||
| `S3_ENDPOINT`| Optional custom S3 endpoint host.
|
||||
| `S3_USE_PATH_STYLE_ENDPOINTS`| `true` or `false`
|
||||
| `AWS_ACCESS_KEY_ID` | S3 access key ID
|
||||
| `AWS_SECRET_ACCESS_KEY` | S3 secret access key ID
|
||||
| `MAX_FILE_SIZE` | Maximum upload file size in bytes (defaults to 2147483648)
|
||||
| `MAX_EXPIRE_SECONDS` | Maximum upload expiry time in seconds (defaults to 604800)
|
||||
| `REDIS_HOST` | Host name of the Redis server.
|
||||
| `SENTRY_CLIENT` | Sentry Client ID
|
||||
| `SENTRY_DSN` | Sentry DSN
|
||||
| `DETECT_BASE_URL` | Autodetect the base URL using browser if `BASE_URL` is unset (defaults to `false`)
|
||||
| `PORT` | Port the server will listen on (defaults to `1443`)
|
||||
| `NODE_ENV` | Run in `development` mode (unsafe) or `production` mode (the default)
|
||||
| `SEND_FOOTER_DMCA_URL` | A URL to a contact page for DMCA requests (empty / not shown by default)
|
||||
| `SENTRY_CLIENT`, `SENTRY_DSN` | Sentry Client ID and DSN for error tracking (optional, disabled by default)
|
||||
|
||||
## Example:
|
||||
*Note: more options can be found here: https://github.com/timvisee/send/blob/master/server/config.js*
|
||||
|
||||
#### Upload and Download Limits
|
||||
|
||||
Configure the limits for uploads and downloads. Long expiration times are risky on public servers as people may use you as free hosting for copyrighted content or malware (which is why Mozilla shut down their `send` service). It's advised to only expose your service on a LAN/intranet, password protect it with a proxy/gateway, or make sure to set `SEND_FOOTER_DMCA_URL` above so you can respond to takedown requests.
|
||||
|
||||
| Name | Description |
|
||||
|------------------|-------------|
|
||||
| `MAX_FILE_SIZE` | Maximum upload file size in bytes (defaults to `2147483648` aka 2GB)
|
||||
| `MAX_FILES_PER_ARCHIVE` | Maximum number of files per archive (defaults to `64`)
|
||||
| `MAX_EXPIRE_SECONDS` | Maximum upload expiry time in seconds (defaults to `604800` aka 7 days)
|
||||
| `MAX_DOWNLOADS` | Maximum number of downloads (defaults to `100`)
|
||||
| `DOWNLOAD_COUNTS` | Download limit options to show in UI dropdown, e.g. `10,1,2,5,10,15,25,50,100,1000`
|
||||
| `EXPIRE_TIMES_SECONDS` | Expire time options to show in UI dropdown, e.g. `3600,86400,604800,2592000,31536000`
|
||||
| `DEFAULT_DOWNLOADS` | Default download limit in UI (defaults to `1`)
|
||||
| `DEFAULT_EXPIRE_SECONDS` | Default expire time in UI (defaults to `86400`)
|
||||
|
||||
*Note: more options can be found here: https://github.com/timvisee/send/blob/master/server/config.js*
|
||||
|
||||
#### Storage Backend Options
|
||||
|
||||
Pick how you want to store uploaded files and set these config options accordingly:
|
||||
|
||||
- Local filesystem (the default): set `FILE_DIR` to the local path used inside the container for storage (or leave the default)
|
||||
- S3-compatible object store: set `S3_BUCKET`, `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY` (and `S3_ENDPOINT` if using something other than AWS)
|
||||
- Google Cloud Storage: set `GCS_BUCKET` to the name of a GCS bucket (auth should be set up using [Application Default Credentials](https://cloud.google.com/docs/authentication/production#auth-cloud-implicit-nodejs))
|
||||
|
||||
Redis is used as the metadata database for the backend and is required no matter which storage method you use.
|
||||
|
||||
| Name | Description |
|
||||
|------------------|-------------|
|
||||
| `REDIS_HOST`, `REDIS_PORT`, `REDIS_USER`, `REDIS_PASSWORD`, `REDIS_DB` | Host name, port, and pass of the Redis server (defaults to `localhost`, `6379`, and no password)
|
||||
| `FILE_DIR` | Directory for storage inside the Docker container (defaults to `/uploads`)
|
||||
| `S3_BUCKET` | The S3 bucket name to use (only set if using S3 for storage)
|
||||
| `S3_ENDPOINT` | An optional custom endpoint to use for S3 (defaults to AWS)
|
||||
| `S3_USE_PATH_STYLE_ENDPOINT`| Whether to force [path style URLs](https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/Config.html#s3ForcePathStyle-property) for S3 objects (defaults to `false`)
|
||||
| `AWS_ACCESS_KEY_ID` | S3 access key ID (only set if using S3 for storage)
|
||||
| `AWS_SECRET_ACCESS_KEY` | S3 secret access key ID (only set if using S3 for storage)
|
||||
| `GCS_BUCKET` | Google Cloud Storage bucket (only set if using GCP for storage)
|
||||
|
||||
*Note: more options can be found here: https://github.com/timvisee/send/blob/master/server/config.js*
|
||||
|
||||
## Branding
|
||||
|
||||
To change the look the colors aswell as some graphics can be changed via environment variables.
|
||||
See the table below for the variables and their default values.
|
||||
|
||||
| Name | Default | Description |
|
||||
|---|---|---|
|
||||
| UI_COLOR_PRIMARY | #0a84ff | The primary color |
|
||||
| UI_COLOR_ACCENT | #003eaa | The accent color (eg. for hover-effects) |
|
||||
| UI_CUSTOM_ASSETS_ANDROID_CHROME_192PX | | A custom icon for Android (192x192px) |
|
||||
| UI_CUSTOM_ASSETS_ANDROID_CHROME_512PX | | A custom icon for Android (512x512px) |
|
||||
| UI_CUSTOM_ASSETS_APPLE_TOUCH_ICON | | A custom icon for Apple |
|
||||
| UI_CUSTOM_ASSETS_FAVICON_16PX | | A custom favicon (16x16px) |
|
||||
| UI_CUSTOM_ASSETS_FAVICON_32PX | | A custom favicon (32x32px) |
|
||||
| UI_CUSTOM_ASSETS_ICON | | A custom icon (Logo on the top-left of the UI) |
|
||||
| UI_CUSTOM_ASSETS_SAFARI_PINNED_TAB | | A custom icon for Safari |
|
||||
| UI_CUSTOM_ASSETS_FACEBOOK | | A custom header image for Facebook |
|
||||
| UI_CUSTOM_ASSETS_TWITTER | | A custom header image for Twitter |
|
||||
| UI_CUSTOM_ASSETS_WORDMARK | | A custom wordmark (Text next to the logo) |
|
||||
| UI_CUSTOM_CSS | | Allows you to define a custom CSS file for custom styling |
|
||||
| CUSTOM_FOOTER_TEXT | | Allows you to define a custom footer |
|
||||
| CUSTOM_FOOTER_URL | | Allows you to define a custom URL in your footer |
|
||||
|
||||
Side note: If you define a custom URL and a custom footer, only the footer text will display, but will be hyperlinked to the URL.
|
||||
|
||||
## Examples
|
||||
|
||||
**Run using an Amazon Elasticache for the Redis DB, Amazon S3 for the storage backend, and Sentry for error reporting.**
|
||||
|
||||
```bash
|
||||
$ docker run --net=host -e 'NODE_ENV=production' \
|
||||
$ docker run -p 1443:1443 \
|
||||
-e 'S3_BUCKET=testpilot-p2p-dev' \
|
||||
-e 'REDIS_HOST=dyf9s2r4vo3.bolxr4.0001.usw2.cache.amazonaws.com' \
|
||||
-e 'SENTRY_CLIENT=https://51e23d7263e348a7a3b90a5357c61cb2@sentry.prod.mozaws.net/168' \
|
||||
-e 'SENTRY_DSN=https://51e23d7263e348a7a3b90a5357c61cb2:65e23d7263e348a7a3b90a5357c61c44@sentry.prod.mozaws.net/168' \
|
||||
-e 'BASE_URL=https://send.firefox.com' \
|
||||
-e 'BASE_URL=https://send.example.com' \
|
||||
registry.gitlab.com/timvisee/send:latest
|
||||
```
|
||||
|
||||
## Docker compose
|
||||
*Note: make sure to replace the example values above with your real values before running.*
|
||||
|
||||
|
||||
**Run totally self-hosted using the current filesystem directry (`$PWD`) to store the Redis data and file uploads, with a `5GB` upload limit, 1 month expiry, and contact URL set.**
|
||||
|
||||
```bash
|
||||
# create a network for the send backend and redis containers to talk to each other
|
||||
$ docker network create timviseesend
|
||||
|
||||
# start the redis container
|
||||
$ docker run --net=timviseesend -v $PWD/redis:/data redis-server --appendonly yes
|
||||
|
||||
# start the send backend container
|
||||
$ docker run --net=timviseesend -v $PWD/uploads:/uploads -p 1443:1443 \
|
||||
-e 'BASE_URL=http://localhost:1443' \
|
||||
-e 'MAX_FILE_SIZE=5368709120' \
|
||||
-e 'MAX_EXPIRE_SECONDS=2592000' \
|
||||
-e 'SEND_FOOTER_DMCA_URL=https://example.com/dmca-contact-info' \
|
||||
registry.gitlab.com/timvisee/send:latest
|
||||
```
|
||||
Then open http://localhost:1443 to view the UI. (change the `localhost` to your IP or hostname above to serve the UI to others)
|
||||
|
||||
To run with HTTPS, you will need to set up a reverse proxy with SSL termination in front of the backend. See Docker Compose below for an example setup.
|
||||
|
||||
|
||||
**Run with custom branding.**
|
||||
|
||||
```bash
|
||||
$ docker run -p 1443:1443 \
|
||||
-v $PWD/custom_assets:/app/dist/custom_assets \
|
||||
-e 'UI_COLOR_PRIMARY=#f00' \
|
||||
-e 'UI_COLOR_ACCENT=#a00' \
|
||||
-e 'UI_CUSTOM_ASSETS_ICON=custom_assets/logo.svg' \
|
||||
registry.gitlab.com/timvisee/send:latest
|
||||
```
|
||||
|
||||
## Docker Compose
|
||||
|
||||
For a Docker compose configuration example, see:
|
||||
|
||||
|
|
18
docs/faq.md
|
@ -1,12 +1,12 @@
|
|||
## How big of a file can I transfer with Send?
|
||||
|
||||
There is a 2.5GB file size limit built in to Send(1GB for non-signed in users), however, in practice you may
|
||||
be unable to send files that large. Send encrypts and decrypts the files in
|
||||
the browser which is great for security but will tax your system resources. In
|
||||
particular you can expect to see your memory usage go up by at least the size
|
||||
of the file when the transfer is processing. You can see [the results of some
|
||||
testing](https://github.com/mozilla/send/issues/170#issuecomment-314107793).
|
||||
For the most reliable operation on common computers, it’s probably best to stay
|
||||
There is a 2GB file size limit built in to Send, but this may be changed by the
|
||||
hoster. Send encrypts and decrypts the files in the browser which is great for
|
||||
security but will tax your system resources. In particular you can expect to
|
||||
see your memory usage go up by at least the size of the file when the transfer
|
||||
is processing. You can see [the results of some
|
||||
testing](https://github.com/mozilla/send/issues/170#issuecomment-314107793). For
|
||||
the most reliable operation on common computers, it’s probably best to stay
|
||||
under a few hundred megabytes.
|
||||
|
||||
## Why is my browser not supported?
|
||||
|
@ -23,10 +23,10 @@ Send uses JavaScript to:
|
|||
|
||||
- Encrypt and decrypt files locally on the client instead of the server.
|
||||
- Render the user interface.
|
||||
- Manage translations on the website into [various different languages](https://github.com/mozilla/send#localization).
|
||||
- Manage translations on the website into [various different languages](https://github.com/timvisee/send#localization).
|
||||
- Collect data to help us improve Send in accordance with our [Terms & Privacy](https://send.firefox.com/legal).
|
||||
|
||||
Since Send is an open source project, you can see all of the cool ways we use JavaScript by [examining our code](https://github.com/mozilla/send/).
|
||||
Since Send is an open source project, you can see all of the cool ways we use JavaScript by [examining our code](https://github.com/timvisee/send/).
|
||||
|
||||
## How long are files available for?
|
||||
|
||||
|
|
35372
package-lock.json
generated
88
package.json
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "send",
|
||||
"description": "File Sharing Experiment",
|
||||
"version": "3.4.6",
|
||||
"version": "3.4.27",
|
||||
"author": "Mozilla (https://mozilla.org)",
|
||||
"contributors": [
|
||||
"Tim Visee <3a4fb3964f@sinenomine.email> (https://timvisee.com)"
|
||||
|
@ -30,16 +30,14 @@
|
|||
"test:report": "nyc report --reporter=html",
|
||||
"test-integration": "cross-env NODE_ENV=development wdio test/wdio.docker.conf.js",
|
||||
"circleci-test-integration": "echo 'webdriverio tests need to be updated to node 12'",
|
||||
"start": "npm run clean && cross-env NODE_ENV=development L10N_DEV=true FXA_CLIENT_ID=fced6b5e3f4c66b9 BASE_URL=http://localhost:8080 webpack-dev-server --mode=development",
|
||||
"start": "npm run clean && cross-env NODE_ENV=development L10N_DEV=true BASE_URL=http://localhost:8080 DETECT_BASE_URL=true webpack-dev-server --mode=development",
|
||||
"android": "cross-env ANDROID=1 npm start",
|
||||
"prod": "node server/bin/prod.js"
|
||||
},
|
||||
"husky": {
|
||||
"hooks": {
|
||||
"pre-commit": "lint-staged",
|
||||
"pre-push": "npm test",
|
||||
"post-merge": "npm install",
|
||||
"post-checkout": "scripts/sync-npm-dependencies.sh"
|
||||
"pre-push": "npm test"
|
||||
}
|
||||
},
|
||||
"lint-staged": {
|
||||
|
@ -61,29 +59,29 @@
|
|||
"cache": true
|
||||
},
|
||||
"engines": {
|
||||
"node": "^15.5.1"
|
||||
"node": "^16.13.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.13.15",
|
||||
"@babel/plugin-proposal-class-properties": "^7.13.0",
|
||||
"@babel/core": "^7.17.9",
|
||||
"@babel/plugin-proposal-class-properties": "^7.16.7",
|
||||
"@babel/plugin-syntax-dynamic-import": "^7.2.0",
|
||||
"@babel/preset-env": "^7.13.15",
|
||||
"@babel/preset-env": "^7.16.11",
|
||||
"@dannycoates/webcrypto-liner": "^0.1.37",
|
||||
"@fullhuman/postcss-purgecss": "^1.3.0",
|
||||
"@fullhuman/postcss-purgecss": "^4.1.3",
|
||||
"@mattiasbuelens/web-streams-polyfill": "0.2.1",
|
||||
"@sentry/browser": "^5.30.0",
|
||||
"asmcrypto.js": "^0.22.0",
|
||||
"babel-loader": "^8.2.2",
|
||||
"babel-loader": "^8.2.4",
|
||||
"babel-plugin-istanbul": "^5.2.0",
|
||||
"base64-js": "^1.5.1",
|
||||
"content-disposition": "^0.5.3",
|
||||
"copy-webpack-plugin": "^5.1.2",
|
||||
"core-js": "^3.10.1",
|
||||
"content-disposition": "^0.5.4",
|
||||
"copy-webpack-plugin": "^6.4.0",
|
||||
"core-js": "^3.21.1",
|
||||
"crc": "^3.8.0",
|
||||
"cross-env": "^6.0.3",
|
||||
"css-loader": "^3.6.0",
|
||||
"css-loader": "^5.2.7",
|
||||
"css-mqpacker": "^7.0.0",
|
||||
"cssnano": "^4.1.11",
|
||||
"cssnano": "^5.1.12",
|
||||
"eslint": "^6.6.0",
|
||||
"eslint-config-prettier": "^6.15.0",
|
||||
"eslint-plugin-mocha": "^6.2.1",
|
||||
|
@ -93,22 +91,23 @@
|
|||
"extract-loader": "^3.2.0",
|
||||
"extract-text-webpack-plugin": "^4.0.0-beta.0",
|
||||
"fast-text-encoding": "^1.0.3",
|
||||
"file-loader": "^4.2.0",
|
||||
"git-rev-sync": "^1.12.0",
|
||||
"file-loader": "^6.2.0",
|
||||
"git-rev-sync": "^3.0.2",
|
||||
"html-loader": "^0.5.5",
|
||||
"http_ece": "^1.1.0",
|
||||
"husky": "^3.0.9",
|
||||
"intl-pluralrules": "^1.2.2",
|
||||
"intl-pluralrules": "^1.3.1",
|
||||
"lint-staged": "^9.4.2",
|
||||
"mocha": "^6.2.2",
|
||||
"mocha": "^10.1.0",
|
||||
"morgan": "^1.9.1",
|
||||
"nanobus": "^4.5.0",
|
||||
"nanohtml": "^1.9.0",
|
||||
"nanotiming": "^7.3.1",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"nyc": "^14.1.1",
|
||||
"postcss-loader": "^3.0.0",
|
||||
"postcss-preset-env": "^6.7.0",
|
||||
"postcss": "^8.4.14",
|
||||
"postcss-loader": "^4.2.0",
|
||||
"postcss-preset-env": "^7.7.2",
|
||||
"prettier": "^1.19.1",
|
||||
"proxyquire": "^2.1.3",
|
||||
"puppeteer": "^2.0.0",
|
||||
|
@ -117,41 +116,42 @@
|
|||
"script-loader": "^0.7.2",
|
||||
"sinon": "^7.5.0",
|
||||
"string-hash": "^1.1.3",
|
||||
"stylelint": "^13.12.0",
|
||||
"stylelint-config-standard": "^19.0.0",
|
||||
"stylelint-no-unsupported-browser-features": "^4.1.4",
|
||||
"svgo": "^1.3.2",
|
||||
"svgo-loader": "^2.2.2",
|
||||
"tailwindcss": "^1.9.6",
|
||||
"val-loader": "^1.1.1",
|
||||
"stylelint": "^14.9.1",
|
||||
"stylelint-config-standard": "^26.0.0",
|
||||
"stylelint-no-unsupported-browser-features": "^5.0.3",
|
||||
"svgo": "^2.8.0",
|
||||
"svgo-loader": "^3.0.1",
|
||||
"tailwindcss": "^2",
|
||||
"val-loader": "^2.1.2",
|
||||
"webpack": "4.38.0",
|
||||
"webpack-cli": "^3.3.12",
|
||||
"webpack-dev-middleware": "^3.7.3",
|
||||
"webpack-dev-server": "^3.11.2",
|
||||
"webpack-dev-server": "^3.11.3",
|
||||
"webpack-manifest-plugin": "^2.2.0",
|
||||
"webpack-unassert-loader": "^1.2.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@dannycoates/express-ws": "^5.0.3",
|
||||
"@fluent/bundle": "^0.13.0",
|
||||
"@fluent/langneg": "^0.3.0",
|
||||
"@google-cloud/storage": "^5.8.3",
|
||||
"@sentry/node": "^5.30.0",
|
||||
"aws-sdk": "^2.884.0",
|
||||
"body-parser": "^1.19.0",
|
||||
"@fluent/bundle": "^0.17.1",
|
||||
"@fluent/langneg": "^0.6.2",
|
||||
"@google-cloud/storage": "^6.2.3",
|
||||
"@sentry/node": "^7.7.0",
|
||||
"aws-sdk": "^2.1109.0",
|
||||
"body-parser": "^1.20.0",
|
||||
"choo": "^7.0.0",
|
||||
"cldr-core": "^35.1.0",
|
||||
"configstore": "github:dannycoates/configstore#master",
|
||||
"convict": "^5.2.0",
|
||||
"express": "^4.17.1",
|
||||
"convict": "^6.2.4",
|
||||
"convict-format-with-validator": "^6.2.0",
|
||||
"double-ended-queue": "^2.1.0-0",
|
||||
"express": "^4.17.3",
|
||||
"helmet": "^3.23.3",
|
||||
"mkdirp": "^0.5.1",
|
||||
"mozlog": "^2.2.0",
|
||||
"node-fetch": "^2.6.1",
|
||||
"redis": "^2.8.0",
|
||||
"mozlog": "^3.0.1",
|
||||
"node-fetch": "^2.6.7",
|
||||
"redis": "^3.1.1",
|
||||
"redis-mock": "^0.47.0",
|
||||
"selenium-standalone": "^6.23.0",
|
||||
"ua-parser-js": "^0.7.28"
|
||||
"selenium-standalone": "^6.24.0",
|
||||
"ua-parser-js": "^0.7.31"
|
||||
},
|
||||
"availableLanguages": [
|
||||
"en-US",
|
||||
|
|
|
@ -1,8 +1,6 @@
|
|||
class TailwindExtractor {
|
||||
static extract(content) {
|
||||
return content.match(/[A-Za-z0-9-_:/]+/g) || [];
|
||||
}
|
||||
}
|
||||
const TailwindExtractor = content => {
|
||||
return content.match(/[A-Za-z0-9-_:/]+/g) || [];
|
||||
};
|
||||
|
||||
const options = {
|
||||
plugins: [
|
||||
|
|
|
@ -1,23 +1,18 @@
|
|||
{
|
||||
"name": "firefox-send",
|
||||
"name": "send",
|
||||
"description": "File Sharing Experiment",
|
||||
"repository": {
|
||||
"url": "https://github.com/mozilla/send/",
|
||||
"url": "https://github.com/timvisee/send/",
|
||||
"license": "MPL-2.0"
|
||||
},
|
||||
"participate": {
|
||||
"home": "https://github.com/mozilla/send/blob/master/README.md",
|
||||
"docs": "https://github.com/mozilla/send/blob/master/README.md"
|
||||
},
|
||||
"bugs": {
|
||||
"list": "https://github.com/mozilla/send/issues",
|
||||
"report": "https://github.com/mozilla/send/issues/new"
|
||||
},
|
||||
"urls": {
|
||||
"prod": "https://send.firefox.com/",
|
||||
"stage": "https://stage.send.nonprod.cloudops.mozgcp.net/",
|
||||
"dev": "https://send2.dev.lcip.org/"
|
||||
},
|
||||
"participate": {
|
||||
"home": "https://github.om/timvisee/send/blob/master/README.md",
|
||||
"docs": "https://github.com/timvisee/send/blob/master/README.md"
|
||||
},
|
||||
"bugs": {
|
||||
"list": "https://github.com/timvisee/send/issues",
|
||||
"report": "https://github.com/timvisee/send/issues/new"
|
||||
},
|
||||
"keywords": [
|
||||
"JavaScript",
|
||||
"jQuery",
|
||||
|
|
|
@ -48,12 +48,12 @@ passwordSetError = Toto heslo nemohlo být nastaveno
|
|||
-send-brand =
|
||||
{ $case ->
|
||||
*[nom] Send
|
||||
[gen] Send
|
||||
[dat] Send
|
||||
[gen] Sendu
|
||||
[dat] Sendu
|
||||
[acc] Send
|
||||
[voc] Send
|
||||
[loc] Send
|
||||
[ins] Send
|
||||
[voc] Sende
|
||||
[loc] Sendu
|
||||
[ins] Sendem
|
||||
}
|
||||
-send-short-brand =
|
||||
{ $case ->
|
||||
|
@ -86,7 +86,7 @@ passwordSetError = Toto heslo nemohlo být nastaveno
|
|||
[ins] Mozillou
|
||||
}
|
||||
introTitle = Jednoduché a soukromé sdílení souborů
|
||||
introDescription = S { -send-brand(case: "ins") } jsou sdílené soubory šifrované end-to-end, takže ani my nevíme, co sdílíte. Platnost odkazů je navíc omezená. Soubory tak můžete sdílet soukromě a s jistotou, že se nezůstanou na internetu válet navždy.
|
||||
introDescription = Se { -send-brand(case: "ins") } jsou sdílené soubory šifrované end-to-end, takže ani my nevíme, co sdílíte. Platnost odkazů je navíc omezená. Soubory tak můžete sdílet soukromě a s jistotou, že se nezůstanou na internetu válet navždy.
|
||||
notifyUploadEncryptDone = Váš soubor je zašifrovaný a připraven k odeslání
|
||||
# downloadCount is from the downloadCount string and timespan is a timespanMinutes string. ex. 'Expires after 2 downloads or 25 minutes'
|
||||
archiveExpiryInfo = Platnost vyprší po { $downloadCount } nebo za { $timespan }
|
||||
|
@ -131,7 +131,7 @@ copyLinkDescription = Soubor můžete sdílet tímto odkazem:
|
|||
copyLinkButton = Zkopírovat odkaz
|
||||
downloadTitle = Stáhnout soubory
|
||||
downloadDescription = Tento soubor byl sdílen přes { -send-brand(case: "acc") } s end-to-end šifrováním a odkazem s omezenou platností.
|
||||
trySendDescription = Vyzkoušejte jednoduché a bezpečné sdílení souborů s { -send-brand(case: "ins") }
|
||||
trySendDescription = Vyzkoušejte jednoduché a bezpečné sdílení souborů se { -send-brand(case: "ins") }
|
||||
# count will always be > 10
|
||||
tooManyFiles =
|
||||
{ $count ->
|
||||
|
@ -189,6 +189,6 @@ downloadFirefoxPromo = { -send-short-brand } od aplikace { -firefox }.
|
|||
shareLinkDescription = Sdílet odkaz na soubor:
|
||||
shareLinkButton = Sdílet odkaz
|
||||
# $name is the name of the file
|
||||
shareMessage = Stáhněte si soubor „{ $name }“ s { -send-brand(case: "ins") } - jednoduché a bezpečné sdílení souborů
|
||||
shareMessage = Stáhněte si soubor „{ $name }“ se { -send-brand(case: "ins") } - jednoduché a bezpečné sdílení souborů
|
||||
trailheadPromo = Existuje způsob, jak ochránit své soukromí. Používejte Firefox.
|
||||
learnMore = Zjistit více.
|
||||
|
|
|
@ -147,3 +147,5 @@ shareLinkButton = Share link
|
|||
shareMessage = Download “{ $name }” with { -send-brand }: simple, safe file sharing
|
||||
trailheadPromo = There is a way to protect your privacy. Join Firefox.
|
||||
learnMore = Learn more.
|
||||
|
||||
sponsoredByThunderbird = Sponsored by Thunderbird
|
||||
|
|
|
@ -152,3 +152,5 @@ shareLinkButton = Share link
|
|||
shareMessage = Download “{ $name }” with { -send-brand }: simple, safe file sharing
|
||||
trailheadPromo = There is a way to protect your privacy. Join Firefox.
|
||||
learnMore = Learn more.
|
||||
|
||||
sponsoredByThunderbird = Sponsored by Thunderbird
|
||||
|
|
|
@ -143,3 +143,5 @@ shareLinkButton = Share link
|
|||
# $name is the name of the file
|
||||
shareMessage = Download “{ $name }” with { -send-brand }: simple, safe file sharing
|
||||
learnMore = Learn more.
|
||||
|
||||
sponsoredByThunderbird = Sponsored by Thunderbird
|
||||
|
|
|
@ -28,7 +28,7 @@ notSupportedOutdatedDetail = Helaas ondersteunt deze versie van Firefox de webte
|
|||
updateFirefox = Firefox bijwerken
|
||||
deletePopupCancel = Annuleren
|
||||
deleteButtonHover = Verwijderen
|
||||
footerText = Niet aangesloten aan Mozilla of Firefox.
|
||||
footerText = Niet gelieerd aan Mozilla of Firefox.
|
||||
footerLinkDonate = Doneren
|
||||
footerLinkCli = CLI
|
||||
footerLinkDmca = DMCA
|
||||
|
@ -52,7 +52,7 @@ passwordSetError = Dit wachtwoord kon niet worden ingesteld
|
|||
-send-short-brand = Send
|
||||
-firefox = Firefox
|
||||
-mozilla = Mozilla
|
||||
introTitle = Eenvoudig, privé bestanden delen
|
||||
introTitle = Bestanden delen, eenvoudig en privé
|
||||
introDescription = Met { -send-brand } kunt u bestanden delen met end-to-endversleuteling en een koppeling die automatisch verloopt. Hierdoor kunt u privé houden wat u wilt delen en er zeker van zijn dat uw zaken niet voor altijd online blijven.
|
||||
notifyUploadEncryptDone = Uw bestand is versleuteld en klaar voor verzending
|
||||
# downloadCount is from the downloadCount string and timespan is a timespanMinutes string. ex. 'Expires after 2 downloads or 25 minutes'
|
||||
|
@ -152,3 +152,5 @@ shareLinkButton = Koppeling delen
|
|||
shareMessage = Download ‘{ $name }’ met { -send-brand }: eenvoudig, veilig bestanden delen
|
||||
trailheadPromo = Er is een manier om uw privacy te beschermen. Doe mee met Firefox.
|
||||
learnMore = Meer info.
|
||||
|
||||
sponsoredByThunderbird = Gesponsord door Thunderbird
|
||||
|
|
|
@ -53,7 +53,7 @@ introTitle = Простой и безопасный обмен файлами
|
|||
introDescription = { -send-brand } позволяет вам делиться файлами со сквозным шифрованием и ограниченным сроком действия ссылки на загрузку. Так что, вы сможете делиться файлами приватно и они не останутся в сети навсегда.
|
||||
notifyUploadEncryptDone = Ваш файл зашифрован и готов к отправке
|
||||
# downloadCount is from the downloadCount string and timespan is a timespanMinutes string. ex. 'Expires after 2 downloads or 25 minutes'
|
||||
archiveExpiryInfo = Срок хранения истекает после { $downloadCount } или через { $timespan }
|
||||
archiveExpiryInfo = Удалить после { $downloadCount } или через { $timespan }
|
||||
timespanMinutes =
|
||||
{ $num ->
|
||||
[one] { $num } минуту
|
||||
|
@ -89,7 +89,7 @@ gb = ГБ
|
|||
# localized number and byte abbreviation. example "2.5MB"
|
||||
fileSize = { $num }{ $units }
|
||||
# $size is the size of the file, displayed using the fileSize message as format (e.g. "2.5MB")
|
||||
totalSize = Общий размер: { $size }
|
||||
totalSize = Всего: { $size }
|
||||
# the next line after the colon contains a file name
|
||||
copyLinkDescription = Скопируйте ссылку, чтобы поделиться своим файлом:
|
||||
copyLinkButton = Копировать ссылку
|
||||
|
@ -117,13 +117,13 @@ legalTitle = Уведомление о конфиденциальности { -s
|
|||
legalDateStamp = Версия 1.0, от 12 марта 2019 года
|
||||
# A short representation of a countdown timer containing the number of days, hours, and minutes remaining as digits, example "2d 11h 56m"
|
||||
expiresDaysHoursMinutes = { $days } дн. { $hours } ч. { $minutes } мин.
|
||||
addFilesButton = Выберите файлы для выгрузки
|
||||
addFilesButton = Добавить
|
||||
uploadButton = Выгрузить
|
||||
# the first part of the string 'Drag and drop files or click to send up to 1GB'
|
||||
dragAndDropFiles = Перетащите файлы сюда
|
||||
# the second part of the string 'Drag and drop files or click to send up to 1GB'
|
||||
# $size is the size of the file, displayed using the fileSize message as format (e.g. "2.5MB")
|
||||
orClickWithSize = или щёлкните здесь, чтобы отправить их (до { $size })
|
||||
orClickWithSize = или кликните сюда для отправки файлов до { $size }
|
||||
addPassword = Защитить паролем
|
||||
emailPlaceholder = Введите ваш адрес электронной почты
|
||||
# $size is the size of the file, displayed using the fileSize message as format (e.g. "2.5MB")
|
||||
|
|
|
@ -2,12 +2,12 @@ title = Send
|
|||
importingFile = 正在导入…
|
||||
encryptingFile = 正在加密…
|
||||
decryptingFile = 正在解密…
|
||||
downloadCount =
|
||||
{ $num ->
|
||||
downloadCount = { $num ->
|
||||
[one] 1 次下载
|
||||
*[other] { $num } 次下载
|
||||
}
|
||||
timespanHours =
|
||||
{ $num ->
|
||||
timespanHours = { $num ->
|
||||
[one] 1 小时
|
||||
*[other] { $num } 小时
|
||||
}
|
||||
copiedUrl = 已复制!
|
||||
|
@ -26,6 +26,11 @@ notSupportedOutdatedDetail = 很可惜,此版本的 Firefox 不支持 Send 所
|
|||
updateFirefox = 更新 Firefox
|
||||
deletePopupCancel = 取消
|
||||
deleteButtonHover = 删除
|
||||
footerText = 不附属于 Mozilla 或 Firefox。
|
||||
footerLinkDonate = 捐助
|
||||
footerLinkCli = 命令行
|
||||
footerLinkDmca = DMCA
|
||||
footerLinkSource = 源代码
|
||||
passwordTryAgain = 密码不正确。请重试。
|
||||
javascriptRequired = Send 需要 JavaScript
|
||||
whyJavascript = 为什么 Send 需要 JavaScript?
|
||||
|
@ -142,5 +147,4 @@ shareLinkDescription = 您的文件链接:
|
|||
shareLinkButton = 分享链接
|
||||
# $name is the name of the file
|
||||
shareMessage = 使用 { -send-brand } 下载“{ $name }”:简单、安全的文件分享服务
|
||||
trailheadPromo = 捍卫隐私不是幻想。加入 Firefox 一同抗争。
|
||||
learnMore = 详细了解。
|
||||
|
|
|
@ -2,12 +2,12 @@ title = Send
|
|||
importingFile = 匯入中…
|
||||
encryptingFile = 加密中…
|
||||
decryptingFile = 解密中…
|
||||
downloadCount =
|
||||
{ $num ->
|
||||
downloadCount = { $num ->
|
||||
[one] 1 次下載
|
||||
*[other] { $num } 次下載
|
||||
}
|
||||
timespanHours =
|
||||
{ $num ->
|
||||
timespanHours = { $num ->
|
||||
[one] 1 小時
|
||||
*[other] { $num } 小時
|
||||
}
|
||||
copiedUrl = 已複製!
|
||||
|
@ -26,6 +26,11 @@ notSupportedOutdatedDetail = 很可惜,此版本的 Firefox 不支援 Send 所
|
|||
updateFirefox = 更新 Firefox
|
||||
deletePopupCancel = 取消
|
||||
deleteButtonHover = 刪除
|
||||
footerText = 不隸屬於 Mozilla 或 Firefox。
|
||||
footerLinkDonate = 捐助
|
||||
footerLinkCli = 命令列
|
||||
footerLinkDmca = DMCA
|
||||
footerLinkSource = 原始碼
|
||||
passwordTryAgain = 密碼不正確,請再試一次。
|
||||
javascriptRequired = Send 需要開啟 JavaScript 功能
|
||||
whyJavascript = 為什麼 Send 需要 JavaScript 才能使用?
|
||||
|
@ -45,27 +50,28 @@ passwordSetError = 無法設定此密碼
|
|||
-send-short-brand = Send
|
||||
-firefox = Firefox
|
||||
-mozilla = Mozilla
|
||||
|
||||
introTitle = 簡單而私密的檔案共享服務
|
||||
introDescription = { -send-brand } 讓您可透過點對點加密的方式來分享檔案,並提供會自動失效的鏈結。這樣一來就可以保留分享時的隱私,也確保檔案不會永久保存於網路上。
|
||||
notifyUploadEncryptDone = 已加密您的檔案,可以傳送
|
||||
# downloadCount is from the downloadCount string and timespan is a timespanMinutes string. ex. 'Expires after 2 downloads or 25 minutes'
|
||||
archiveExpiryInfo = { $downloadCount } 或 { $timespan } 後失效
|
||||
timespanMinutes =
|
||||
{ $num ->
|
||||
timespanMinutes = { $num ->
|
||||
[one] 1 分鐘
|
||||
*[other] { $num } 分鐘
|
||||
}
|
||||
timespanDays =
|
||||
{ $num ->
|
||||
timespanDays = { $num ->
|
||||
[one] 1 天
|
||||
*[other] { $num } 天
|
||||
}
|
||||
timespanWeeks =
|
||||
{ $num ->
|
||||
timespanWeeks = { $num ->
|
||||
[one] 1 週
|
||||
*[other] { $num } 週
|
||||
}
|
||||
fileCount =
|
||||
{ $num ->
|
||||
*[other] { $num } 個檔案
|
||||
}
|
||||
fileCount = { $num ->
|
||||
[one] 1 個檔案
|
||||
*[other] { $num } 個檔案
|
||||
}
|
||||
# byte abbreviation
|
||||
bytes = 位元組
|
||||
# kibibyte abbreviation
|
||||
|
@ -85,15 +91,15 @@ downloadTitle = 下載檔案
|
|||
downloadDescription = 此檔案是透過 { -send-brand } 進行分享,以點對點加密的方式來分享檔案,並提供會自動失效的鏈結。
|
||||
trySendDescription = 快試試 { -send-brand },簡單安全的檔案分享機制。
|
||||
# count will always be > 10
|
||||
tooManyFiles =
|
||||
{ $count ->
|
||||
*[other] 一次僅能上傳 { $count } 個檔案。
|
||||
}
|
||||
tooManyFiles = { $count ->
|
||||
[one] 一次僅能上傳 1 個檔案。
|
||||
*[other] 一次僅能上傳 { $count } 個檔案。
|
||||
}
|
||||
# count will always be > 10
|
||||
tooManyArchives =
|
||||
{ $count ->
|
||||
*[other] 僅允許 { $count } 個壓縮檔。
|
||||
}
|
||||
tooManyArchives = { $count ->
|
||||
[one] 僅允許 1 個壓縮檔。
|
||||
*[other] 僅允許 { $count } 個壓縮檔。
|
||||
}
|
||||
expiredTitle = 此鏈結已經失效。
|
||||
notSupportedDescription = 無法於此瀏覽器使用 { -send-brand }。在最新版的 { -firefox } 中使用 { -send-short-brand } 會有最佳效果,也可在大部分瀏覽器的最新版本當中使用。
|
||||
downloadFirefox = 下載 { -firefox }
|
||||
|
@ -136,5 +142,4 @@ shareLinkDescription = 您的檔案鏈結:
|
|||
shareLinkButton = 分享鏈結
|
||||
# $name is the name of the file
|
||||
shareMessage = 使用 { -send-brand } 下載「{ $name }」: 簡單安全的檔案分享機制
|
||||
trailheadPromo = 有種方法可以保護您的隱私,加入 Firefox。
|
||||
learnMore = 了解更多。
|
||||
|
|
2
public/robots.txt
Normal file
|
@ -0,0 +1,2 @@
|
|||
User-agent: *
|
||||
Disallow: /
|
|
@ -12,9 +12,22 @@ module.exports = {
|
|||
FOOTER_DONATE_URL: config.footer_donate_url,
|
||||
FOOTER_CLI_URL: config.footer_cli_url,
|
||||
FOOTER_DMCA_URL: config.footer_dmca_url,
|
||||
FOOTER_SOURCE_URL: config.footer_source_url
|
||||
FOOTER_SOURCE_URL: config.footer_source_url,
|
||||
CUSTOM_FOOTER_TEXT: config.custom_footer_text,
|
||||
CUSTOM_FOOTER_URL: config.custom_footer_url,
|
||||
MAIN_NOTICE_HTML: config.main_notice_html,
|
||||
UPLOAD_AREA_NOTICE_HTML: config.upload_area_notice_html,
|
||||
UPLOADS_LIST_NOTICE_HTML: config.uploads_list_notice_html,
|
||||
DOWNLOAD_NOTICE_HTML: config.download_notice_html,
|
||||
SHOW_THUNDERBIRD_SPONSOR: config.show_thunderbird_sponsor,
|
||||
COLORS: {
|
||||
PRIMARY: config.ui_color_primary,
|
||||
ACCENT: config.ui_color_accent
|
||||
},
|
||||
CUSTOM_ASSETS: config.ui_custom_assets
|
||||
},
|
||||
DEFAULTS: {
|
||||
DOWNLOADS: config.default_downloads,
|
||||
DOWNLOAD_COUNTS: config.download_counts,
|
||||
EXPIRE_TIMES_SECONDS: config.expire_times_seconds,
|
||||
EXPIRE_SECONDS: config.default_expire_seconds
|
||||
|
|
192
server/config.js
|
@ -1,8 +1,31 @@
|
|||
const convict = require('convict');
|
||||
const convict_format_with_validator = require('convict-format-with-validator');
|
||||
const { tmpdir } = require('os');
|
||||
const path = require('path');
|
||||
const { randomBytes } = require('crypto');
|
||||
|
||||
convict.addFormats(convict_format_with_validator);
|
||||
|
||||
convict.addFormat({
|
||||
name: 'positive-int-array',
|
||||
coerce: ints => {
|
||||
// can take: int[] | string[] | string (csv), returns -> int[]
|
||||
const ints_arr = Array.isArray(ints) ? ints : ints.trim().split(',');
|
||||
return ints_arr.map(int =>
|
||||
typeof int === 'number'
|
||||
? int
|
||||
: parseInt(int.replace(/['"]+/g, '').trim(), 10)
|
||||
);
|
||||
},
|
||||
validate: ints => {
|
||||
// takes: int[], errors if any NaNs, negatives, or floats present
|
||||
for (const int of ints) {
|
||||
if (typeof int !== 'number' || isNaN(int) || int < 0 || int % 1 > 0)
|
||||
throw new Error('must be a comma-separated list of positive integers');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const conf = convict({
|
||||
s3_bucket: {
|
||||
format: String,
|
||||
|
@ -25,7 +48,7 @@ const conf = convict({
|
|||
env: 'GCS_BUCKET'
|
||||
},
|
||||
expire_times_seconds: {
|
||||
format: Array,
|
||||
format: 'positive-int-array',
|
||||
default: [300, 3600, 86400, 604800],
|
||||
env: 'EXPIRE_TIMES_SECONDS'
|
||||
},
|
||||
|
@ -40,10 +63,15 @@ const conf = convict({
|
|||
env: 'MAX_EXPIRE_SECONDS'
|
||||
},
|
||||
download_counts: {
|
||||
format: Array,
|
||||
format: 'positive-int-array',
|
||||
default: [1, 2, 3, 4, 5, 20, 50, 100],
|
||||
env: 'DOWNLOAD_COUNTS'
|
||||
},
|
||||
default_downloads: {
|
||||
format: Number,
|
||||
default: 1,
|
||||
env: 'DEFAULT_DOWNLOADS'
|
||||
},
|
||||
max_downloads: {
|
||||
format: Number,
|
||||
default: 100,
|
||||
|
@ -64,6 +92,26 @@ const conf = convict({
|
|||
default: 'localhost',
|
||||
env: 'REDIS_HOST'
|
||||
},
|
||||
redis_port: {
|
||||
format: Number,
|
||||
default: 6379,
|
||||
env: 'REDIS_PORT'
|
||||
},
|
||||
redis_user: {
|
||||
format: String,
|
||||
default: '',
|
||||
env: 'REDIS_USER'
|
||||
},
|
||||
redis_password: {
|
||||
format: String,
|
||||
default: '',
|
||||
env: 'REDIS_PASSWORD'
|
||||
},
|
||||
redis_db: {
|
||||
format: String,
|
||||
default: '',
|
||||
env: 'REDIS_DB'
|
||||
},
|
||||
redis_event_expire: {
|
||||
format: Boolean,
|
||||
default: false,
|
||||
|
@ -117,9 +165,25 @@ const conf = convict({
|
|||
},
|
||||
base_url: {
|
||||
format: 'url',
|
||||
default: 'https://send.firefox.com',
|
||||
default: 'https://send.example.com',
|
||||
env: 'BASE_URL'
|
||||
},
|
||||
custom_title: {
|
||||
format: String,
|
||||
default: 'Send',
|
||||
env: 'CUSTOM_TITLE'
|
||||
},
|
||||
custom_description: {
|
||||
format: String,
|
||||
default:
|
||||
'Encrypt and send files with a link that automatically expires to ensure your important documents don’t stay online forever.',
|
||||
env: 'CUSTOM_DESCRIPTION'
|
||||
},
|
||||
detect_base_url: {
|
||||
format: Boolean,
|
||||
default: false,
|
||||
env: 'DETECT_BASE_URL'
|
||||
},
|
||||
file_dir: {
|
||||
format: 'String',
|
||||
default: `${tmpdir()}${path.sep}send-${randomBytes(4).toString('hex')}`,
|
||||
|
@ -189,6 +253,113 @@ const conf = convict({
|
|||
format: String,
|
||||
default: 'https://github.com/timvisee/send',
|
||||
env: 'SEND_FOOTER_SOURCE_URL'
|
||||
},
|
||||
custom_footer_text: {
|
||||
format: String,
|
||||
default: '',
|
||||
env: 'CUSTOM_FOOTER_TEXT'
|
||||
},
|
||||
custom_footer_url: {
|
||||
format: String,
|
||||
default: '',
|
||||
env: 'CUSTOM_FOOTER_URL'
|
||||
},
|
||||
main_notice_html: {
|
||||
format: String,
|
||||
default: '',
|
||||
env: 'SEND_MAIN_NOTICE_HTML'
|
||||
},
|
||||
upload_area_notice_html: {
|
||||
format: String,
|
||||
default: '',
|
||||
env: 'SEND_UPLOAD_AREA_NOTICE_HTML'
|
||||
},
|
||||
uploads_list_notice_html: {
|
||||
format: String,
|
||||
default: '',
|
||||
env: 'SEND_UPLOADS_LIST_NOTICE_HTML'
|
||||
},
|
||||
download_notice_html: {
|
||||
format: String,
|
||||
default: '',
|
||||
env: 'SEND_DOWNLOAD_NOTICE_HTML'
|
||||
},
|
||||
show_thunderbird_sponsor: {
|
||||
format: Boolean,
|
||||
default: false,
|
||||
env: 'SHOW_THUNDERBIRD_SPONSOR'
|
||||
},
|
||||
ui_color_primary: {
|
||||
format: String,
|
||||
default: '#0a84ff',
|
||||
env: 'UI_COLOR_PRIMARY'
|
||||
},
|
||||
ui_color_accent: {
|
||||
format: String,
|
||||
default: '#003eaa',
|
||||
env: 'UI_COLOR_ACCENT'
|
||||
},
|
||||
custom_locale: {
|
||||
format: String,
|
||||
default: '',
|
||||
env: 'CUSTOM_LOCALE'
|
||||
},
|
||||
ui_custom_assets: {
|
||||
android_chrome_192px: {
|
||||
format: String,
|
||||
default: '',
|
||||
env: 'UI_CUSTOM_ASSETS_ANDROID_CHROME_192PX'
|
||||
},
|
||||
android_chrome_512px: {
|
||||
format: String,
|
||||
default: '',
|
||||
env: 'UI_CUSTOM_ASSETS_ANDROID_CHROME_512PX'
|
||||
},
|
||||
apple_touch_icon: {
|
||||
format: String,
|
||||
default: '',
|
||||
env: 'UI_CUSTOM_ASSETS_APPLE_TOUCH_ICON'
|
||||
},
|
||||
favicon_16px: {
|
||||
format: String,
|
||||
default: '',
|
||||
env: 'UI_CUSTOM_ASSETS_FAVICON_16PX'
|
||||
},
|
||||
favicon_32px: {
|
||||
format: String,
|
||||
default: '',
|
||||
env: 'UI_CUSTOM_ASSETS_FAVICON_32PX'
|
||||
},
|
||||
icon: {
|
||||
format: String,
|
||||
default: '',
|
||||
env: 'UI_CUSTOM_ASSETS_ICON'
|
||||
},
|
||||
safari_pinned_tab: {
|
||||
format: String,
|
||||
default: '',
|
||||
env: 'UI_CUSTOM_ASSETS_SAFARI_PINNED_TAB'
|
||||
},
|
||||
facebook: {
|
||||
format: String,
|
||||
default: '',
|
||||
env: 'UI_CUSTOM_ASSETS_FACEBOOK'
|
||||
},
|
||||
twitter: {
|
||||
format: String,
|
||||
default: '',
|
||||
env: 'UI_CUSTOM_ASSETS_TWITTER'
|
||||
},
|
||||
wordmark: {
|
||||
format: String,
|
||||
default: '',
|
||||
env: 'UI_CUSTOM_ASSETS_WORDMARK'
|
||||
},
|
||||
custom_css: {
|
||||
format: String,
|
||||
default: '',
|
||||
env: 'UI_CUSTOM_CSS'
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -196,4 +367,17 @@ const conf = convict({
|
|||
conf.validate({ allowed: 'strict' });
|
||||
|
||||
const props = conf.getProperties();
|
||||
module.exports = props;
|
||||
|
||||
const deriveBaseUrl = req => {
|
||||
if (!props.detect_base_url) {
|
||||
return props.base_url;
|
||||
}
|
||||
|
||||
const protocol = req.secure ? 'https://' : 'http://';
|
||||
return `${protocol}${req.headers.host}`;
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
...props,
|
||||
deriveBaseUrl
|
||||
};
|
||||
|
|
|
@ -3,6 +3,10 @@ const assets = require('../common/assets');
|
|||
const initScript = require('./initScript');
|
||||
|
||||
module.exports = function(state, body = '') {
|
||||
const custom_css = state.ui.assets.custom_css !== ''
|
||||
? html`<link rel="stylesheet" type="text/css" href="${state.ui.assets.custom_css}" />`
|
||||
: ''
|
||||
|
||||
return html`
|
||||
<!DOCTYPE html>
|
||||
<html lang="${state.locale}">
|
||||
|
@ -21,45 +25,46 @@ module.exports = function(state, body = '') {
|
|||
<meta property="og:description" content="${state.description}" />
|
||||
<meta name="twitter:description" content="${state.description}" />
|
||||
<meta name="twitter:card" content="summary" />
|
||||
<meta
|
||||
property="og:image"
|
||||
content="${state.baseUrl}/${assets.get('send-fb.jpg')}"
|
||||
/>
|
||||
<meta
|
||||
name="twitter:image"
|
||||
content="${state.baseUrl}/${assets.get('send-twitter.jpg')}"
|
||||
/>
|
||||
<meta property="og:image" content="${state.ui.assets.facebook}" />
|
||||
<meta name="twitter:image" content="${state.ui.assets.twitter}" />
|
||||
<meta property="og:url" content="${state.baseUrl}" />
|
||||
<meta name="theme-color" content="#220033" />
|
||||
<meta name="msapplication-TileColor" content="#220033" />
|
||||
|
||||
<link rel="manifest" href="/app.webmanifest" />
|
||||
<link rel="stylesheet" type="text/css" href="/inter.css" />
|
||||
<style nonce=${state.cspNonce}>
|
||||
:root {
|
||||
--color-primary: ${state.ui.colors.primary};
|
||||
--color-primary-accent: ${state.ui.colors.accent};
|
||||
}
|
||||
</style>
|
||||
<link
|
||||
rel="stylesheet"
|
||||
type="text/css"
|
||||
href="${assets.get('app.css')}"
|
||||
/>
|
||||
${custom_css}
|
||||
<link
|
||||
rel="apple-touch-icon"
|
||||
sizes="180x180"
|
||||
href="${assets.get('apple-touch-icon.png')}"
|
||||
href="${state.ui.assets.apple_touch_icon}"
|
||||
/>
|
||||
<link
|
||||
rel="icon"
|
||||
type="image/png"
|
||||
sizes="32x32"
|
||||
href="${assets.get('favicon-32x32.png')}"
|
||||
href="${state.ui.assets.favicon_32px}"
|
||||
/>
|
||||
<link
|
||||
rel="icon"
|
||||
type="image/png"
|
||||
sizes="16x16"
|
||||
href="${assets.get('favicon-16x16.png')}"
|
||||
href="${state.ui.assets.favicon_16px}"
|
||||
/>
|
||||
<link
|
||||
rel="mask-icon"
|
||||
href="${assets.get('safari-pinned-tab.svg')}"
|
||||
href="${state.ui.assets.safari_pinned_tab}"
|
||||
color="#838383"
|
||||
/>
|
||||
<script defer src="${assets.get('app.js')}"></script>
|
||||
|
|
|
@ -1,13 +1,15 @@
|
|||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { FluentBundle } = require('@fluent/bundle');
|
||||
const { FluentBundle, FluentResource } = require('@fluent/bundle');
|
||||
const localesPath = path.resolve(__dirname, '../public/locales');
|
||||
const locales = fs.readdirSync(localesPath);
|
||||
|
||||
function makeBundle(locale) {
|
||||
const bundle = new FluentBundle(locale, { useIsolating: false });
|
||||
bundle.addMessages(
|
||||
fs.readFileSync(path.resolve(localesPath, locale, 'send.ftl'), 'utf8')
|
||||
bundle.addResource(
|
||||
new FluentResource(
|
||||
fs.readFileSync(path.resolve(localesPath, locale, 'send.ftl'), 'utf8')
|
||||
)
|
||||
);
|
||||
return [locale, bundle];
|
||||
}
|
||||
|
@ -19,8 +21,11 @@ module.exports = function getTranslator(locale) {
|
|||
const bundle = bundles.get(locale) || defaultBundle;
|
||||
return function(id, data) {
|
||||
if (bundle.hasMessage(id)) {
|
||||
return bundle.format(bundle.getMessage(id), data);
|
||||
return bundle.formatPattern(bundle.getMessage(id).value, data);
|
||||
}
|
||||
return defaultBundle.format(defaultBundle.getMessage(id), data);
|
||||
return defaultBundle.formatPattern(
|
||||
defaultBundle.getMessage(id).value,
|
||||
data
|
||||
);
|
||||
};
|
||||
};
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
const assert = require('assert');
|
||||
const crypto = require('crypto');
|
||||
const storage = require('../storage');
|
||||
const config = require('../config');
|
||||
const fxa = require('../fxa');
|
||||
|
||||
module.exports = {
|
||||
|
@ -70,10 +71,11 @@ module.exports = {
|
|||
const token = authHeader.split(' ')[1];
|
||||
req.user = await fxa.verify(token);
|
||||
}
|
||||
if (req.user) {
|
||||
next();
|
||||
} else {
|
||||
|
||||
if (config.fxa_required && !req.user) {
|
||||
res.sendStatus(401);
|
||||
} else {
|
||||
next();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
|
@ -36,15 +36,26 @@ module.exports = function(app) {
|
|||
defaultSrc: ["'self'"],
|
||||
connectSrc: [
|
||||
"'self'",
|
||||
config.base_url.replace(/^https:\/\//, 'wss://')
|
||||
function(req) {
|
||||
const baseUrl = config.deriveBaseUrl(req);
|
||||
const r = baseUrl.replace(/^http(s?):\/\//, 'ws$1://');
|
||||
console.log([baseUrl, r]);
|
||||
return r;
|
||||
}
|
||||
],
|
||||
imgSrc: ["'self'"],
|
||||
imgSrc: ["'self'", 'data:'],
|
||||
scriptSrc: [
|
||||
"'self'",
|
||||
function(req) {
|
||||
return `'nonce-${req.cspNonce}'`;
|
||||
}
|
||||
],
|
||||
styleSrc: [
|
||||
"'self'",
|
||||
function(req) {
|
||||
return `'nonce-${req.cspNonce}'`;
|
||||
}
|
||||
],
|
||||
formAction: ["'none'"],
|
||||
frameAncestors: ["'none'"],
|
||||
objectSrc: ["'none'"],
|
||||
|
@ -52,10 +63,6 @@ module.exports = function(app) {
|
|||
}
|
||||
};
|
||||
|
||||
csp.directives.connectSrc.push(
|
||||
config.base_url.replace(/^https:\/\//, 'wss://')
|
||||
);
|
||||
|
||||
app.use(helmet.contentSecurityPolicy(csp));
|
||||
}
|
||||
|
||||
|
|
|
@ -28,8 +28,7 @@ module.exports = async function(req, res) {
|
|||
//this hasn't been updated to expiration time setting yet
|
||||
//if you want to fallback to this code add this
|
||||
await storage.set(newId, fileStream, meta, config.default_expire_seconds);
|
||||
const protocol = config.env === 'production' ? 'https' : req.protocol;
|
||||
const url = `${protocol}://${req.get('host')}/download/${newId}/`;
|
||||
const url = `${config.deriveBaseUrl(req)}/download/${newId}/`;
|
||||
res.set('WWW-Authenticate', `send-v1 ${meta.nonce}`);
|
||||
res.json({
|
||||
url,
|
||||
|
|
|
@ -1,18 +1,20 @@
|
|||
const assets = require('../../common/assets');
|
||||
const state = require('../state');
|
||||
|
||||
module.exports = async function(req, res) {
|
||||
const appState = await state(req);
|
||||
|
||||
module.exports = function(req, res) {
|
||||
const manifest = {
|
||||
name: 'Send',
|
||||
short_name: 'Send',
|
||||
lang: req.language,
|
||||
icons: [
|
||||
{
|
||||
src: assets.get('android-chrome-192x192.png'),
|
||||
src: appState.ui.assets.android_chrome_192px,
|
||||
type: 'image/png',
|
||||
sizes: '192x192'
|
||||
},
|
||||
{
|
||||
src: assets.get('android-chrome-512x512.png'),
|
||||
src: appState.ui.assets.android_chrome_512px,
|
||||
type: 'image/png',
|
||||
sizes: '512x512'
|
||||
}
|
||||
|
|
|
@ -26,7 +26,7 @@ module.exports = function(ws, req) {
|
|||
|
||||
const fileInfo = JSON.parse(message);
|
||||
const timeLimit = fileInfo.timeLimit || config.default_expire_seconds;
|
||||
const dlimit = fileInfo.dlimit || 1;
|
||||
const dlimit = fileInfo.dlimit || config.default_downloads;
|
||||
const metadata = fileInfo.fileMetadata;
|
||||
const auth = fileInfo.authorization;
|
||||
const user = await fxa.verify(fileInfo.bearer);
|
||||
|
@ -65,8 +65,7 @@ module.exports = function(ws, req) {
|
|||
nonce: crypto.randomBytes(16).toString('base64')
|
||||
};
|
||||
|
||||
const protocol = config.env === 'production' ? 'https' : req.protocol;
|
||||
const url = `${protocol}://${req.get('host')}/download/${newId}/`;
|
||||
const url = `${config.deriveBaseUrl(req)}/download/${newId}/`;
|
||||
|
||||
ws.send(
|
||||
JSON.stringify({
|
||||
|
|
|
@ -3,9 +3,18 @@ const layout = require('./layout');
|
|||
const assets = require('../common/assets');
|
||||
const getTranslator = require('./locale');
|
||||
const { getFxaConfig } = require('./fxa');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
module.exports = async function(req) {
|
||||
const locale = req.language || 'en-US';
|
||||
const locale = (() => {
|
||||
if (config.custom_locale != '' && fs.existsSync(path.join(__dirname,'../public/locales',config.custom_locale))) {
|
||||
return config.custom_locale;
|
||||
}
|
||||
else {
|
||||
return req.language || 'en-US';
|
||||
}
|
||||
})();
|
||||
let authConfig = null;
|
||||
let robots = 'none';
|
||||
if (req.route && req.route.path === '/') {
|
||||
|
@ -23,6 +32,24 @@ module.exports = async function(req) {
|
|||
if (config.survey_url) {
|
||||
prefs.surveyUrl = config.survey_url;
|
||||
}
|
||||
const baseUrl = config.deriveBaseUrl(req);
|
||||
const uiAssets = {
|
||||
android_chrome_192px: assets.get('android-chrome-192x192.png'),
|
||||
android_chrome_512px: assets.get('android-chrome-512x512.png'),
|
||||
apple_touch_icon: assets.get('apple-touch-icon.png'),
|
||||
favicon_16px: assets.get('favicon-16x16.png'),
|
||||
favicon_32px: assets.get('favicon-32x32.png'),
|
||||
icon: assets.get('icon.svg'),
|
||||
safari_pinned_tab: assets.get('safari-pinned-tab.svg'),
|
||||
facebook: baseUrl + '/' + assets.get('send-fb.jpg'),
|
||||
twitter: baseUrl + '/' + assets.get('send-twitter.jpg'),
|
||||
wordmark: assets.get('wordmark.svg') + '#logo',
|
||||
custom_css: ''
|
||||
};
|
||||
Object.keys(uiAssets).forEach(index => {
|
||||
if (config.ui_custom_assets[index] !== '')
|
||||
uiAssets[index] = config.ui_custom_assets[index];
|
||||
});
|
||||
return {
|
||||
archive: {
|
||||
numFiles: 0
|
||||
|
@ -30,11 +57,16 @@ module.exports = async function(req) {
|
|||
locale,
|
||||
capabilities: { account: false },
|
||||
translate: getTranslator(locale),
|
||||
title: 'Send',
|
||||
description:
|
||||
'Encrypt and send files with a link that automatically expires to ensure your important documents don’t stay online forever.',
|
||||
baseUrl: config.base_url,
|
||||
ui: {},
|
||||
title: config.custom_title,
|
||||
description: config.custom_description,
|
||||
baseUrl,
|
||||
ui: {
|
||||
colors: {
|
||||
primary: config.ui_color_primary,
|
||||
accent: config.ui_color_accent
|
||||
},
|
||||
assets: uiAssets
|
||||
},
|
||||
storage: {
|
||||
files: []
|
||||
},
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const promisify = require('util').promisify;
|
||||
const mkdirp = require('mkdirp');
|
||||
|
||||
const stat = promisify(fs.stat);
|
||||
|
||||
|
@ -9,7 +8,9 @@ class FSStorage {
|
|||
constructor(config, log) {
|
||||
this.log = log;
|
||||
this.dir = config.file_dir;
|
||||
mkdirp.sync(this.dir);
|
||||
fs.mkdirSync(this.dir, {
|
||||
recursive: true
|
||||
});
|
||||
}
|
||||
|
||||
async length(id) {
|
||||
|
|
|
@ -8,8 +8,10 @@ module.exports = function(config) {
|
|||
|
||||
//eslint-disable-next-line security/detect-non-literal-require
|
||||
const redis = require(redis_lib);
|
||||
const client = redis.createClient({
|
||||
|
||||
var client_config = {
|
||||
host: config.redis_host,
|
||||
port: config.redis_port,
|
||||
retry_strategy: options => {
|
||||
if (options.total_retry_time > config.redis_retry_time) {
|
||||
client.emit('error', 'Retry time exhausted');
|
||||
|
@ -18,7 +20,14 @@ module.exports = function(config) {
|
|||
|
||||
return config.redis_retry_delay;
|
||||
}
|
||||
});
|
||||
};
|
||||
if (config.redis_user != null && config.redis_user.length > 0)
|
||||
client_config.user = config.redis_user;
|
||||
if (config.redis_password != null && config.redis_password.length > 0)
|
||||
client_config.password = config.redis_password;
|
||||
if (config.redis_db != null && config.redis_db.length > 0)
|
||||
client_config.db = config.redis_db;
|
||||
const client = redis.createClient(client_config);
|
||||
|
||||
client.ttlAsync = promisify(client.ttl);
|
||||
client.hgetallAsync = promisify(client.hgetall);
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
const colors = {
|
||||
transparent: 'transparent',
|
||||
primary: 'var(--color-primary)',
|
||||
primary_accent: 'var(--color-primary-accent)',
|
||||
|
||||
black: '#000000',
|
||||
'grey-90': '#0c0c0d',
|
||||
|
@ -16,93 +18,12 @@ const colors = {
|
|||
'grey-10': '#f9f9fa',
|
||||
white: '#ffffff',
|
||||
|
||||
'red-90': '#3e0200',
|
||||
'red-80': '#5a0002',
|
||||
'red-70': '#a4000f',
|
||||
'red-60': '#d70022',
|
||||
red: '#d70022',
|
||||
'red-50': '#ff0039',
|
||||
// unspec
|
||||
'red-40': '#ff3363',
|
||||
'red-30': '#ff99aa',
|
||||
|
||||
'orange-90': '#3e1300',
|
||||
'orange-80': '#712b00',
|
||||
'orange-70': '#a44900',
|
||||
'orange-60': '#d76e00',
|
||||
'orange-50': '#ff9400',
|
||||
// unspec
|
||||
'orange-40': '#ffb24c',
|
||||
'orange-30': '#ffd399',
|
||||
|
||||
'yellow-90': '#3e2800',
|
||||
'yellow-80': '#715100',
|
||||
'yellow-70': '#a47f00',
|
||||
'yellow-60': '#d7b600',
|
||||
yellow: '#d7b600',
|
||||
'yellow-50': '#ffe900',
|
||||
'yellow-40': '#ffed4c',
|
||||
'yellow-30': '#fff599',
|
||||
|
||||
// 'green-darkest': '#003706',
|
||||
// 'green-darker': '#006504',
|
||||
// 'green-dark': '#058b00',
|
||||
// green: '#12bc00',
|
||||
// 'green-light': '#51d88a',
|
||||
// 'green-lighter': '#a2f5bf',
|
||||
// 'green-lightest': '#e3fcec',
|
||||
|
||||
// 'teal-darkest': '#0d3331',
|
||||
// 'teal-darker': '#20504f',
|
||||
// 'teal-dark': '#38a89d',
|
||||
// teal: '#4dc0b5',
|
||||
// 'teal-light': '#64d5ca',
|
||||
// 'teal-lighter': '#a0f0ed',
|
||||
// 'teal-lightest': '#e8fffe',
|
||||
|
||||
'blue-90': '#000f40',
|
||||
'blue-80': '#002275',
|
||||
'blue-70': '#003eaa',
|
||||
'blue-60': '#0060df',
|
||||
'blue-50': '#0a84ff',
|
||||
blue: '#0a84ff',
|
||||
'blue-40': '#45a1ff',
|
||||
'blue-30': '#99ccff',
|
||||
'blue-20': '#cce6ff',
|
||||
|
||||
'ink-90': '#0f1126',
|
||||
'ink-80': '#202340',
|
||||
'ink-70': '#363959',
|
||||
|
||||
// 'indigo-darkest': '#191e38',
|
||||
// 'indigo-darker': '#2f365f',
|
||||
// 'indigo-dark': '#5661b3',
|
||||
// indigo: '#6574cd',
|
||||
// 'indigo-light': '#7886d7',
|
||||
// 'indigo-lighter': '#b2b7ff',
|
||||
// 'indigo-lightest': '#e6e8ff',
|
||||
|
||||
'purple-90': '#25003e',
|
||||
'purple-80': '#440071',
|
||||
'purple-70': '#6200a4',
|
||||
'purple-60': '#8000d7',
|
||||
'purple-50': '#9400ff',
|
||||
'purple-40': '#ad3bff',
|
||||
'purple-30': '#c069ff',
|
||||
'purple-20': '#d7a3ff',
|
||||
|
||||
// 'pink-darkest': '#451225',
|
||||
// 'pink-darker': '#6f213f',
|
||||
// 'pink-dark': '#eb5286',
|
||||
// pink: '#f66d9b',
|
||||
// 'pink-light': '#fa7ea8',
|
||||
// 'pink-lighter': '#ffbbca',
|
||||
// 'pink-lightest': '#ffebef',
|
||||
cloud: 'rgba(255, 255, 255, 0.8)',
|
||||
violet: 'hsl(258, 57%, 35%)'
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
purge: false,
|
||||
theme: {
|
||||
colors: colors,
|
||||
screens: {
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
/* eslint-disable no-undef, no-process-exit */
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const mkdirp = require('mkdirp');
|
||||
const puppeteer = require('puppeteer');
|
||||
const webpack = require('webpack');
|
||||
const config = require('../../webpack.config');
|
||||
|
@ -44,7 +43,9 @@ const server = app.listen(async function() {
|
|||
const coverage = await page.evaluate(() => __coverage__);
|
||||
if (coverage) {
|
||||
const dir = path.resolve(__dirname, '../../.nyc_output');
|
||||
mkdirp.sync(dir);
|
||||
fs.mkdirSync(dir, {
|
||||
recursive: true
|
||||
});
|
||||
fs.writeFileSync(
|
||||
path.resolve(dir, 'frontend.json'),
|
||||
JSON.stringify(coverage)
|
||||
|
|
|
@ -1,9 +1,11 @@
|
|||
const path = require('path');
|
||||
const mkdirp = require('mkdirp');
|
||||
const fs = require('fs');
|
||||
const rimraf = require('rimraf');
|
||||
const dir = path.join(__dirname, 'integration', 'downloads');
|
||||
|
||||
mkdirp.sync(dir);
|
||||
fs.mkdirSync(dir, {
|
||||
recursive: true
|
||||
});
|
||||
rimraf.sync(`${dir}${path.sep}*`);
|
||||
|
||||
exports.config = {
|
||||
|
|
|
@ -6,6 +6,13 @@ const VersionPlugin = require('./build/version_plugin');
|
|||
const AndroidIndexPlugin = require('./build/android_index_plugin');
|
||||
const ExtractTextPlugin = require('extract-text-webpack-plugin');
|
||||
|
||||
// Fix for node 18+
|
||||
// See: <https://stackoverflow.com/a/78005686/1000145>
|
||||
const crypto = require('crypto');
|
||||
const crypto_orig_createHash = crypto.createHash;
|
||||
crypto.createHash = algorithm =>
|
||||
crypto_orig_createHash(algorithm == 'md4' ? 'sha256' : algorithm);
|
||||
|
||||
const webJsOptions = {
|
||||
babelrc: false,
|
||||
presets: [
|
||||
|
@ -42,7 +49,8 @@ const serviceWorker = {
|
|||
test: /\.(png|jpg)$/,
|
||||
loader: 'file-loader',
|
||||
options: {
|
||||
name: '[name].[contenthash:8].[ext]'
|
||||
name: '[name].[contenthash:8].[ext]',
|
||||
esModule: false
|
||||
}
|
||||
},
|
||||
{
|
||||
|
@ -51,16 +59,26 @@ const serviceWorker = {
|
|||
{
|
||||
loader: 'file-loader',
|
||||
options: {
|
||||
name: '[name].[contenthash:8].[ext]'
|
||||
name: '[name].[contenthash:8].[ext]',
|
||||
esModule: false
|
||||
}
|
||||
},
|
||||
{
|
||||
loader: 'svgo-loader',
|
||||
options: {
|
||||
plugins: [
|
||||
{ removeViewBox: false }, // true causes stretched images
|
||||
{ convertStyleToAttrs: true }, // for CSP, no unsafe-eval
|
||||
{ removeTitle: true } // for smallness
|
||||
{
|
||||
name: 'removeViewBox',
|
||||
active: false // true causes stretched images
|
||||
},
|
||||
{
|
||||
name: 'convertStyleToAttrs',
|
||||
active: true // for CSP, no unsafe-eval
|
||||
},
|
||||
{
|
||||
name: 'removeTitle',
|
||||
active: true // for smallness
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
@ -127,7 +145,8 @@ const web = {
|
|||
test: /\.(png|jpg)$/,
|
||||
loader: 'file-loader',
|
||||
options: {
|
||||
name: '[name].[contenthash:8].[ext]'
|
||||
name: '[name].[contenthash:8].[ext]',
|
||||
esModule: false
|
||||
}
|
||||
},
|
||||
{
|
||||
|
@ -136,17 +155,30 @@ const web = {
|
|||
{
|
||||
loader: 'file-loader',
|
||||
options: {
|
||||
name: '[name].[contenthash:8].[ext]'
|
||||
name: '[name].[contenthash:8].[ext]',
|
||||
esModule: false
|
||||
}
|
||||
},
|
||||
{
|
||||
loader: 'svgo-loader',
|
||||
options: {
|
||||
plugins: [
|
||||
{ cleanupIDs: false },
|
||||
{ removeViewBox: false }, // true causes stretched images
|
||||
{ convertStyleToAttrs: true }, // for CSP, no unsafe-eval
|
||||
{ removeTitle: true } // for smallness
|
||||
{
|
||||
name: 'cleanupIDs',
|
||||
active: false
|
||||
},
|
||||
{
|
||||
name: 'removeViewBox',
|
||||
active: false // true causes stretched images
|
||||
},
|
||||
{
|
||||
name: 'convertStyleToAttrs',
|
||||
active: true // for CSP, no unsafe-eval
|
||||
},
|
||||
{
|
||||
name: 'removeTitle',
|
||||
active: true // for smallness
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
@ -160,7 +192,8 @@ const web = {
|
|||
{
|
||||
loader: 'css-loader',
|
||||
options: {
|
||||
importLoaders: 1
|
||||
importLoaders: 1,
|
||||
esModule: false
|
||||
}
|
||||
},
|
||||
'postcss-loader'
|
||||
|
@ -184,12 +217,14 @@ const web = {
|
|||
]
|
||||
},
|
||||
plugins: [
|
||||
new CopyPlugin([
|
||||
{
|
||||
context: 'public',
|
||||
from: '*.*'
|
||||
}
|
||||
]),
|
||||
new CopyPlugin({
|
||||
patterns: [
|
||||
{
|
||||
context: 'public',
|
||||
from: '*.*'
|
||||
}
|
||||
]
|
||||
}),
|
||||
new webpack.EnvironmentPlugin(['NODE_ENV']),
|
||||
new webpack.IgnorePlugin(/\.\.\/dist/), // used in common/*.js
|
||||
new ExtractTextPlugin({
|
||||
|
|