mirror of
https://gitlab.com/timvisee/send.git
synced 2025-12-06 22:20:55 +03:00
Compare commits
37 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6dafdcdebd | ||
|
|
dc03b42b96 | ||
|
|
3e07f648b3 | ||
|
|
f58597cece | ||
|
|
d3f9b82672 | ||
|
|
a0bc20aeb6 | ||
|
|
d03e83dd66 | ||
|
|
94e80ccee9 | ||
|
|
f8358c4dac | ||
|
|
ec3cff63a2 | ||
|
|
8f192482b5 | ||
|
|
808a04b669 | ||
|
|
71a925a674 | ||
|
|
64d9cd694d | ||
|
|
94b78b425f | ||
|
|
42e94139a2 | ||
|
|
6bd6280fb5 | ||
|
|
1f2c524b40 | ||
|
|
854810c242 | ||
|
|
45024d3dc6 | ||
|
|
0806b8fd9d | ||
|
|
2dbc740998 | ||
|
|
5b9c8301c7 | ||
|
|
42506dda9d | ||
|
|
8e868a642c | ||
|
|
638f68334a | ||
|
|
d2907c6d8b | ||
|
|
4b05a2f705 | ||
|
|
6960cc75fa | ||
|
|
7f3da34318 | ||
|
|
4369baa258 | ||
|
|
6f1942a446 | ||
|
|
b6d2e7c1ca | ||
|
|
58dd5b7a70 | ||
|
|
4f3a2e4fc1 | ||
|
|
a798b14620 | ||
|
|
2bc1a13ae6 |
@@ -1,4 +1,4 @@
|
|||||||
image: "node:12-slim"
|
image: "node:15-slim"
|
||||||
|
|
||||||
stages:
|
stages:
|
||||||
- test
|
- test
|
||||||
@@ -17,6 +17,7 @@ before_script:
|
|||||||
- apt-get update
|
- 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
|
- 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:
|
test:
|
||||||
stage: test
|
stage: test
|
||||||
script:
|
script:
|
||||||
@@ -24,48 +25,81 @@ test:
|
|||||||
- npm run lint
|
- npm run lint
|
||||||
- npm test
|
- npm test
|
||||||
|
|
||||||
# Release Docker image artifact for easy testing
|
# Build Docker image, export Docker image artifact
|
||||||
artifact-docker:
|
artifact-docker:
|
||||||
stage: artifact
|
stage: artifact
|
||||||
image: docker:latest
|
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:
|
services:
|
||||||
- docker:dind
|
- docker:dind
|
||||||
only:
|
only:
|
||||||
- master
|
- 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: []
|
before_script: []
|
||||||
script:
|
script:
|
||||||
- export IMG_NAME=registry.gitlab.com/timvisee/send:master-$CI_COMMIT_SHA
|
|
||||||
|
|
||||||
# Login in to registry
|
# Login in to registry
|
||||||
- 'docker login registry.gitlab.com -u $DOCKER_USER -p $DOCKER_PASS'
|
- 'docker login registry.gitlab.com -u $DOCKER_USER -p $DOCKER_PASS'
|
||||||
|
|
||||||
# Build and push image, report image name
|
# Load existing, retag for new image images
|
||||||
- docker build -t $IMG_NAME .
|
- docker image load -i $IMG_IMPORT_FILE
|
||||||
- docker push $IMG_NAME
|
- docker tag $IMG_IMPORT_NAME $IMG_NAME
|
||||||
- 'echo Docker image artifact published, available as:'
|
|
||||||
- 'echo " docker pull $IMG_NAME"'
|
|
||||||
|
|
||||||
# Release public Docker image
|
# 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:
|
release-docker:
|
||||||
stage: release
|
stage: release
|
||||||
image: docker:latest
|
image: docker:latest
|
||||||
|
dependencies:
|
||||||
|
- artifact-docker
|
||||||
services:
|
services:
|
||||||
- docker:dind
|
- docker:dind
|
||||||
only:
|
only:
|
||||||
- /^v(\d+\.)*\d+$/
|
- /^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: []
|
before_script: []
|
||||||
script:
|
script:
|
||||||
- export IMG_NAME=registry.gitlab.com/timvisee/send:$CI_COMMIT_REF_NAME
|
|
||||||
- export IMG_NAME_LATEST=registry.gitlab.com/timvisee/send:latest
|
|
||||||
|
|
||||||
# Login in to registry
|
# Login in to registry
|
||||||
- 'docker login registry.gitlab.com -u $DOCKER_USER -p $DOCKER_PASS'
|
- 'docker login registry.gitlab.com -u $DOCKER_USER -p $DOCKER_PASS'
|
||||||
|
|
||||||
# Build and push image, report image name
|
# Load existing, retag for new image images
|
||||||
- docker build -t $IMG_NAME .
|
- docker image load -i $IMG_IMPORT_FILE
|
||||||
- docker tag $IMG_NAME $IMG_NAME_LATEST
|
- 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
|
||||||
- docker push $IMG_NAME_LATEST
|
- docker push $IMG_NAME_LATEST
|
||||||
- 'echo Docker image artifact published, available as:'
|
|
||||||
- 'echo " docker pull $IMG_NAME_LATEST"'
|
- 'echo "Docker image artifact published, available as:" && echo " docker pull $IMG_NAME_LATEST" && echo " docker pull $IMG_NAME"'
|
||||||
- 'echo " docker pull $IMG_NAME"'
|
|
||||||
|
|||||||
12
Dockerfile
12
Dockerfile
@@ -6,13 +6,13 @@
|
|||||||
|
|
||||||
|
|
||||||
# Build project
|
# Build project
|
||||||
FROM node:12 AS builder
|
FROM node:15.5.1-alpine AS builder
|
||||||
RUN set -x \
|
RUN set -x \
|
||||||
# Add user
|
# Add user
|
||||||
&& addgroup --gid 10001 app \
|
&& addgroup --gid 10001 app \
|
||||||
&& adduser --disabled-password \
|
&& adduser --disabled-password \
|
||||||
--gecos '' \
|
--gecos '' \
|
||||||
--gid 10001 \
|
--ingroup app \
|
||||||
--home /app \
|
--home /app \
|
||||||
--uid 10001 \
|
--uid 10001 \
|
||||||
app
|
app
|
||||||
@@ -26,19 +26,17 @@ RUN set -x \
|
|||||||
|
|
||||||
|
|
||||||
# Main image
|
# Main image
|
||||||
FROM node:12-slim
|
FROM node:15.5.1-alpine
|
||||||
RUN set -x \
|
RUN set -x \
|
||||||
# Add user
|
# Add user
|
||||||
&& addgroup --gid 10001 app \
|
&& addgroup --gid 10001 app \
|
||||||
&& adduser --disabled-password \
|
&& adduser --disabled-password \
|
||||||
--gecos '' \
|
--gecos '' \
|
||||||
--gid 10001 \
|
--ingroup app \
|
||||||
--home /app \
|
--home /app \
|
||||||
--uid 10001 \
|
--uid 10001 \
|
||||||
app
|
app
|
||||||
RUN apt-get update && apt-get -y install \
|
|
||||||
git-core \
|
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
|
||||||
USER app
|
USER app
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
COPY --chown=app:app package*.json ./
|
COPY --chown=app:app package*.json ./
|
||||||
|
|||||||
28
README.md
28
README.md
@@ -53,7 +53,7 @@ Thanks [Mozilla][mozilla] for building this amazing tool!
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
**Docs:** [FAQ](docs/faq.md), [Encryption](docs/encryption.md), [Build](docs/build.md), [Docker](docs/docker.md), [Metrics](docs/metrics.md), [More](docs/)
|
**Docs:** [FAQ](docs/faq.md), [Encryption](docs/encryption.md), [Build](docs/build.md), [Docker](docs/docker.md), [More](docs/)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -66,9 +66,8 @@ Thanks [Mozilla][mozilla] for building this amazing tool!
|
|||||||
* [Configuration](#configuration)
|
* [Configuration](#configuration)
|
||||||
* [Localization](#localization)
|
* [Localization](#localization)
|
||||||
* [Contributing](#contributing)
|
* [Contributing](#contributing)
|
||||||
* [Testing](#testing)
|
|
||||||
* [Deployment](#deployment)
|
* [Deployment](#deployment)
|
||||||
* [Android](#android)
|
* [Clients](#clients)
|
||||||
* [License](#license)
|
* [License](#license)
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -133,25 +132,24 @@ Pull requests are always welcome! Feel free to check out the list of ["good firs
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Testing
|
|
||||||
|
|
||||||
| ENVIRONMENT | URL
|
|
||||||
|-------------|-----
|
|
||||||
| Production | <https://send.firefox.com/>
|
|
||||||
| Stage | <https://stage.send.nonprod.cloudops.mozgcp.net/>
|
|
||||||
| Development | <https://send2.dev.lcip.org/>
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Deployment
|
## Deployment
|
||||||
|
|
||||||
see also [docs/deployment.md](docs/deployment.md)
|
see also [docs/deployment.md](docs/deployment.md)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Android
|
## Clients
|
||||||
|
|
||||||
The android implementation is contained in the `android` directory, and can be viewed locally for easy testing and editing by running `ANDROID=1 npm start` and then visiting <http://localhost:8080>. CSS and image files are located in the `android/app/src/main/assets` directory.
|
- Web: _this repository_
|
||||||
|
- Command-line: [`ffsend`](https://github.com/timvisee/ffsend)
|
||||||
|
- Android: _see [Android](#android) section_
|
||||||
|
|
||||||
|
#### Android
|
||||||
|
|
||||||
|
The android implementation is contained in the `android` directory,
|
||||||
|
and can be viewed locally for easy testing and editing by running `ANDROID=1 npm
|
||||||
|
start` and then visiting <http://localhost:8080>. CSS and image files are
|
||||||
|
located in the `android/app/src/main/assets` directory.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import html from 'choo/html';
|
|||||||
import * as Sentry from '@sentry/browser';
|
import * as Sentry from '@sentry/browser';
|
||||||
|
|
||||||
import { setApiUrlPrefix, getConstants } from '../app/api';
|
import { setApiUrlPrefix, getConstants } from '../app/api';
|
||||||
import metrics from '../app/metrics';
|
|
||||||
//import assets from '../common/assets';
|
//import assets from '../common/assets';
|
||||||
import Archive from '../app/archive';
|
import Archive from '../app/archive';
|
||||||
import Header from '../app/ui/header';
|
import Header from '../app/ui/header';
|
||||||
@@ -69,9 +68,10 @@ function body(main) {
|
|||||||
(async function start() {
|
(async function start() {
|
||||||
const translate = await getTranslator('en-US');
|
const translate = await getTranslator('en-US');
|
||||||
setTranslate(translate);
|
setTranslate(translate);
|
||||||
const { LIMITS, DEFAULTS } = await getConstants();
|
const { LIMITS, WEB_UI, DEFAULTS } = await getConstants();
|
||||||
app.use(state => {
|
app.use(state => {
|
||||||
state.LIMITS = LIMITS;
|
state.LIMITS = LIMITS;
|
||||||
|
state.WEB_UI = WEB_UI;
|
||||||
state.DEFAULTS = DEFAULTS;
|
state.DEFAULTS = DEFAULTS;
|
||||||
state.translate = translate;
|
state.translate = translate;
|
||||||
state.capabilities = {
|
state.capabilities = {
|
||||||
@@ -82,7 +82,6 @@ function body(main) {
|
|||||||
state.user = new User(storage, LIMITS);
|
state.user = new User(storage, LIMITS);
|
||||||
state.sentry = Sentry;
|
state.sentry = Sentry;
|
||||||
});
|
});
|
||||||
app.use(metrics);
|
|
||||||
app.route('/', body(home));
|
app.route('/', body(home));
|
||||||
app.route('/upload', upload);
|
app.route('/upload', upload);
|
||||||
app.route('/share/:id', share);
|
app.route('/share/:id', share);
|
||||||
|
|||||||
11
app/api.js
11
app/api.js
@@ -420,17 +420,6 @@ export async function setFileList(bearerToken, kid, data) {
|
|||||||
return response.ok;
|
return response.ok;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function sendMetrics(blob) {
|
|
||||||
if (!navigator.sendBeacon) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
navigator.sendBeacon(getApiUrl('/api/metrics'), blob);
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getConstants() {
|
export async function getConstants() {
|
||||||
const response = await fetch(getApiUrl('/config'));
|
const response = await fetch(getApiUrl('/config'));
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import * as metrics from './metrics';
|
|
||||||
import FileReceiver from './fileReceiver';
|
import FileReceiver from './fileReceiver';
|
||||||
import FileSender from './fileSender';
|
import FileSender from './fileSender';
|
||||||
import copyDialog from './ui/copyDialog';
|
import copyDialog from './ui/copyDialog';
|
||||||
@@ -54,7 +53,6 @@ export default function(state, emitter) {
|
|||||||
|
|
||||||
emitter.on('logout', async () => {
|
emitter.on('logout', async () => {
|
||||||
await state.user.logout();
|
await state.user.logout();
|
||||||
metrics.loggedOut({ trigger: 'button' });
|
|
||||||
emitter.emit('pushState', '/');
|
emitter.emit('pushState', '/');
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -68,14 +66,6 @@ export default function(state, emitter) {
|
|||||||
|
|
||||||
emitter.on('delete', async ownedFile => {
|
emitter.on('delete', async ownedFile => {
|
||||||
try {
|
try {
|
||||||
metrics.deletedUpload({
|
|
||||||
size: ownedFile.size,
|
|
||||||
time: ownedFile.time,
|
|
||||||
speed: ownedFile.speed,
|
|
||||||
type: ownedFile.type,
|
|
||||||
ttl: ownedFile.expiresAt - Date.now(),
|
|
||||||
location
|
|
||||||
});
|
|
||||||
state.storage.remove(ownedFile.id);
|
state.storage.remove(ownedFile.id);
|
||||||
await ownedFile.del();
|
await ownedFile.del();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -101,9 +91,6 @@ export default function(state, emitter) {
|
|||||||
state.LIMITS.MAX_FILES_PER_ARCHIVE
|
state.LIMITS.MAX_FILES_PER_ARCHIVE
|
||||||
);
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e.message === 'fileTooBig' && maxSize < state.LIMITS.MAX_FILE_SIZE) {
|
|
||||||
return emitter.emit('signup-cta', 'size');
|
|
||||||
}
|
|
||||||
state.modal = okDialog(
|
state.modal = okDialog(
|
||||||
state.translate(e.message, {
|
state.translate(e.message, {
|
||||||
size: bytes(maxSize),
|
size: bytes(maxSize),
|
||||||
@@ -123,7 +110,7 @@ export default function(state, emitter) {
|
|||||||
source: query.utm_source,
|
source: query.utm_source,
|
||||||
term: query.utm_term
|
term: query.utm_term
|
||||||
});
|
});
|
||||||
state.modal = signupDialog(source);
|
state.modal = signupDialog();
|
||||||
render();
|
render();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -159,12 +146,9 @@ export default function(state, emitter) {
|
|||||||
|
|
||||||
const links = openLinksInNewTab();
|
const links = openLinksInNewTab();
|
||||||
await delay(200);
|
await delay(200);
|
||||||
const start = Date.now();
|
|
||||||
try {
|
try {
|
||||||
const ownedFile = await sender.upload(archive, state.user.bearerToken);
|
const ownedFile = await sender.upload(archive, state.user.bearerToken);
|
||||||
state.storage.totalUploads += 1;
|
state.storage.totalUploads += 1;
|
||||||
const duration = Date.now() - start;
|
|
||||||
metrics.completedUpload(archive, duration);
|
|
||||||
faviconProgressbar.updateFavicon(0);
|
faviconProgressbar.updateFavicon(0);
|
||||||
|
|
||||||
state.storage.addFile(ownedFile);
|
state.storage.addFile(ownedFile);
|
||||||
@@ -181,7 +165,6 @@ export default function(state, emitter) {
|
|||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (err.message === '0') {
|
if (err.message === '0') {
|
||||||
//cancelled. do nothing
|
//cancelled. do nothing
|
||||||
metrics.cancelledUpload(archive, err.duration);
|
|
||||||
render();
|
render();
|
||||||
} else if (err.message === '401') {
|
} else if (err.message === '401') {
|
||||||
const refreshed = await state.user.refresh();
|
const refreshed = await state.user.refresh();
|
||||||
@@ -197,7 +180,6 @@ export default function(state, emitter) {
|
|||||||
scope.setExtra('size', err.size);
|
scope.setExtra('size', err.size);
|
||||||
state.sentry.captureException(err);
|
state.sentry.captureException(err);
|
||||||
});
|
});
|
||||||
metrics.stoppedUpload(archive, err.duration);
|
|
||||||
emitter.emit('pushState', '/error');
|
emitter.emit('pushState', '/error');
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
@@ -249,13 +231,11 @@ export default function(state, emitter) {
|
|||||||
render();
|
render();
|
||||||
});
|
});
|
||||||
|
|
||||||
emitter.on('download', async file => {
|
emitter.on('download', async () => {
|
||||||
state.transfer.on('progress', updateProgress);
|
state.transfer.on('progress', updateProgress);
|
||||||
state.transfer.on('decrypting', render);
|
state.transfer.on('decrypting', render);
|
||||||
state.transfer.on('complete', render);
|
state.transfer.on('complete', render);
|
||||||
const links = openLinksInNewTab();
|
const links = openLinksInNewTab();
|
||||||
const size = file.size;
|
|
||||||
const start = Date.now();
|
|
||||||
try {
|
try {
|
||||||
const dl = state.transfer.download({
|
const dl = state.transfer.download({
|
||||||
stream: state.capabilities.streamDownload
|
stream: state.capabilities.streamDownload
|
||||||
@@ -263,12 +243,6 @@ export default function(state, emitter) {
|
|||||||
render();
|
render();
|
||||||
await dl;
|
await dl;
|
||||||
state.storage.totalDownloads += 1;
|
state.storage.totalDownloads += 1;
|
||||||
const duration = Date.now() - start;
|
|
||||||
metrics.completedDownload({
|
|
||||||
size,
|
|
||||||
duration,
|
|
||||||
password_protected: file.requiresPassword
|
|
||||||
});
|
|
||||||
faviconProgressbar.updateFavicon(0);
|
faviconProgressbar.updateFavicon(0);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (err.message === '0') {
|
if (err.message === '0') {
|
||||||
@@ -286,12 +260,6 @@ export default function(state, emitter) {
|
|||||||
scope.setExtra('progress', err.progress);
|
scope.setExtra('progress', err.progress);
|
||||||
state.sentry.captureException(err);
|
state.sentry.captureException(err);
|
||||||
});
|
});
|
||||||
const duration = Date.now() - start;
|
|
||||||
metrics.stoppedDownload({
|
|
||||||
size,
|
|
||||||
duration,
|
|
||||||
password_protected: file.requiresPassword
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
emitter.emit('pushState', location);
|
emitter.emit('pushState', location);
|
||||||
}
|
}
|
||||||
@@ -302,7 +270,6 @@ export default function(state, emitter) {
|
|||||||
|
|
||||||
emitter.on('copy', ({ url }) => {
|
emitter.on('copy', ({ url }) => {
|
||||||
copyToClipboard(url);
|
copyToClipboard(url);
|
||||||
// metrics.copiedLink({ location });
|
|
||||||
});
|
});
|
||||||
|
|
||||||
emitter.on('closeModal', () => {
|
emitter.on('closeModal', () => {
|
||||||
|
|||||||
@@ -134,7 +134,7 @@ details[open] > summary > svg {
|
|||||||
transform: rotate(90deg);
|
transform: rotate(90deg);
|
||||||
}
|
}
|
||||||
|
|
||||||
footer li:hover {
|
footer li a:hover {
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
/* global DEFAULTS LIMITS PREFS */
|
/* global DEFAULTS LIMITS WEB_UI PREFS */
|
||||||
import 'core-js';
|
import 'core-js';
|
||||||
import 'fast-text-encoding'; // MS Edge support
|
import 'fast-text-encoding'; // MS Edge support
|
||||||
import 'intl-pluralrules';
|
import 'intl-pluralrules';
|
||||||
@@ -10,7 +10,6 @@ import controller from './controller';
|
|||||||
import dragManager from './dragManager';
|
import dragManager from './dragManager';
|
||||||
import pasteManager from './pasteManager';
|
import pasteManager from './pasteManager';
|
||||||
import storage from './storage';
|
import storage from './storage';
|
||||||
import metrics from './metrics';
|
|
||||||
import experiments from './experiments';
|
import experiments from './experiments';
|
||||||
import * as Sentry from '@sentry/browser';
|
import * as Sentry from '@sentry/browser';
|
||||||
import './main.css';
|
import './main.css';
|
||||||
@@ -51,6 +50,7 @@ if (process.env.NODE_ENV === 'production') {
|
|||||||
window.initialState = {
|
window.initialState = {
|
||||||
LIMITS,
|
LIMITS,
|
||||||
DEFAULTS,
|
DEFAULTS,
|
||||||
|
WEB_UI,
|
||||||
PREFS,
|
PREFS,
|
||||||
archive: new Archive([], DEFAULTS.EXPIRE_SECONDS),
|
archive: new Archive([], DEFAULTS.EXPIRE_SECONDS),
|
||||||
capabilities,
|
capabilities,
|
||||||
@@ -67,7 +67,6 @@ if (process.env.NODE_ENV === 'production') {
|
|||||||
// eslint-disable-next-line require-atomic-updates
|
// eslint-disable-next-line require-atomic-updates
|
||||||
window.app = app;
|
window.app = app;
|
||||||
app.use(experiments);
|
app.use(experiments);
|
||||||
app.use(metrics);
|
|
||||||
app.use(controller);
|
app.use(controller);
|
||||||
app.use(dragManager);
|
app.use(dragManager);
|
||||||
app.use(pasteManager);
|
app.use(pasteManager);
|
||||||
|
|||||||
186
app/metrics.js
186
app/metrics.js
@@ -1,186 +0,0 @@
|
|||||||
import storage from './storage';
|
|
||||||
import { platform, locale } from './utils';
|
|
||||||
import { sendMetrics } from './api';
|
|
||||||
|
|
||||||
let appState = null;
|
|
||||||
let experiment = null;
|
|
||||||
const HOUR = 1000 * 60 * 60;
|
|
||||||
const events = [];
|
|
||||||
let session_id = Date.now();
|
|
||||||
const lang = locale();
|
|
||||||
|
|
||||||
export default function initialize(state, emitter) {
|
|
||||||
appState = state;
|
|
||||||
|
|
||||||
emitter.on('DOMContentLoaded', () => {
|
|
||||||
experiment = storage.enrolled;
|
|
||||||
if (!appState.user.firstAction) {
|
|
||||||
appState.user.firstAction =
|
|
||||||
appState.route === '/' ? 'upload' : 'download';
|
|
||||||
}
|
|
||||||
const query = appState.query;
|
|
||||||
addEvent('client_visit', {
|
|
||||||
entrypoint: appState.route === '/' ? 'upload' : 'download',
|
|
||||||
referrer: document.referrer,
|
|
||||||
utm_campaign: query.utm_campaign,
|
|
||||||
utm_content: query.utm_content,
|
|
||||||
utm_medium: query.utm_medium,
|
|
||||||
utm_source: query.utm_source,
|
|
||||||
utm_term: query.utm_term
|
|
||||||
});
|
|
||||||
});
|
|
||||||
emitter.on('experiment', experimentEvent);
|
|
||||||
window.addEventListener('unload', submitEvents);
|
|
||||||
}
|
|
||||||
|
|
||||||
function sizeOrder(n) {
|
|
||||||
return Math.floor(Math.log10(n));
|
|
||||||
}
|
|
||||||
|
|
||||||
function submitEvents() {
|
|
||||||
if (navigator.doNotTrack === '1') {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
sendMetrics(
|
|
||||||
new Blob(
|
|
||||||
[
|
|
||||||
JSON.stringify({
|
|
||||||
now: Date.now(),
|
|
||||||
session_id,
|
|
||||||
lang,
|
|
||||||
platform: platform(),
|
|
||||||
events
|
|
||||||
})
|
|
||||||
],
|
|
||||||
{ type: 'text/plain' } // see http://crbug.com/490015
|
|
||||||
)
|
|
||||||
);
|
|
||||||
events.splice(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function addEvent(event_type, event_properties) {
|
|
||||||
const user_id = await appState.user.metricId();
|
|
||||||
const device_id = await appState.user.deviceId();
|
|
||||||
const ab_id = Object.keys(experiment)[0];
|
|
||||||
if (ab_id) {
|
|
||||||
event_properties.experiment = ab_id;
|
|
||||||
event_properties.variant = experiment[ab_id];
|
|
||||||
}
|
|
||||||
events.push({
|
|
||||||
device_id,
|
|
||||||
event_properties,
|
|
||||||
event_type,
|
|
||||||
time: Date.now(),
|
|
||||||
user_id,
|
|
||||||
user_properties: {
|
|
||||||
anonymous: !appState.user.loggedIn,
|
|
||||||
first_action: appState.user.firstAction,
|
|
||||||
active_count: storage.files.length
|
|
||||||
}
|
|
||||||
});
|
|
||||||
if (events.length === 25) {
|
|
||||||
submitEvents();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function cancelledUpload(archive, duration) {
|
|
||||||
return addEvent('client_upload', {
|
|
||||||
download_limit: archive.dlimit,
|
|
||||||
duration: sizeOrder(duration),
|
|
||||||
file_count: archive.numFiles,
|
|
||||||
password_protected: !!archive.password,
|
|
||||||
size: sizeOrder(archive.size),
|
|
||||||
status: 'cancel',
|
|
||||||
time_limit: archive.timeLimit
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function completedUpload(archive, duration) {
|
|
||||||
return addEvent('client_upload', {
|
|
||||||
download_limit: archive.dlimit,
|
|
||||||
duration: sizeOrder(duration),
|
|
||||||
file_count: archive.numFiles,
|
|
||||||
password_protected: !!archive.password,
|
|
||||||
size: sizeOrder(archive.size),
|
|
||||||
status: 'ok',
|
|
||||||
time_limit: archive.timeLimit
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function stoppedUpload(archive, duration = 0) {
|
|
||||||
return addEvent('client_upload', {
|
|
||||||
download_limit: archive.dlimit,
|
|
||||||
duration: sizeOrder(duration),
|
|
||||||
file_count: archive.numFiles,
|
|
||||||
password_protected: !!archive.password,
|
|
||||||
size: sizeOrder(archive.size),
|
|
||||||
status: 'error',
|
|
||||||
time_limit: archive.timeLimit
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function stoppedDownload(params) {
|
|
||||||
return addEvent('client_download', {
|
|
||||||
duration: sizeOrder(params.duration),
|
|
||||||
password_protected: params.password_protected,
|
|
||||||
size: sizeOrder(params.size),
|
|
||||||
status: 'error'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function completedDownload(params) {
|
|
||||||
return addEvent('client_download', {
|
|
||||||
duration: sizeOrder(params.duration),
|
|
||||||
password_protected: params.password_protected,
|
|
||||||
size: sizeOrder(params.size),
|
|
||||||
status: 'ok'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function deletedUpload(ownedFile) {
|
|
||||||
return addEvent('client_delete', {
|
|
||||||
age: Math.floor((Date.now() - ownedFile.createdAt) / HOUR),
|
|
||||||
downloaded: ownedFile.dtotal > 0,
|
|
||||||
status: 'ok'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function experimentEvent(params) {
|
|
||||||
return addEvent('client_experiment', params);
|
|
||||||
}
|
|
||||||
|
|
||||||
function submittedSignup(params) {
|
|
||||||
return addEvent('client_login', {
|
|
||||||
status: 'ok',
|
|
||||||
trigger: params.trigger
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function canceledSignup(params) {
|
|
||||||
return addEvent('client_login', {
|
|
||||||
status: 'cancel',
|
|
||||||
trigger: params.trigger
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function loggedOut(params) {
|
|
||||||
addEvent('client_logout', {
|
|
||||||
status: 'ok',
|
|
||||||
trigger: params.trigger
|
|
||||||
});
|
|
||||||
// flush events and start new anon session
|
|
||||||
submitEvents();
|
|
||||||
session_id = Date.now();
|
|
||||||
}
|
|
||||||
|
|
||||||
export {
|
|
||||||
cancelledUpload,
|
|
||||||
stoppedUpload,
|
|
||||||
completedUpload,
|
|
||||||
deletedUpload,
|
|
||||||
stoppedDownload,
|
|
||||||
completedDownload,
|
|
||||||
submittedSignup,
|
|
||||||
canceledSignup,
|
|
||||||
loggedOut
|
|
||||||
};
|
|
||||||
@@ -580,7 +580,7 @@ module.exports.preview = function(state, emit) {
|
|||||||
function download(event) {
|
function download(event) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
event.target.disabled = true;
|
event.target.disabled = true;
|
||||||
emit('download', archive);
|
emit('download');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -13,19 +13,71 @@ class Footer extends Component {
|
|||||||
|
|
||||||
createElement() {
|
createElement() {
|
||||||
const translate = this.state.translate;
|
const translate = this.state.translate;
|
||||||
|
|
||||||
|
// Add additional links from configuration if available
|
||||||
|
var links = [];
|
||||||
|
if (this.state != undefined && this.state.WEB_UI != undefined) {
|
||||||
|
const WEB_UI = this.state.WEB_UI;
|
||||||
|
|
||||||
|
if (WEB_UI.FOOTER_DONATE_URL != '') {
|
||||||
|
links.push(html`
|
||||||
|
<li class="m-2">
|
||||||
|
<a href="${WEB_UI.FOOTER_DONATE_URL}" target="_blank">
|
||||||
|
${translate('footerLinkDonate')}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
if (WEB_UI.FOOTER_CLI_URL != '') {
|
||||||
|
links.push(html`
|
||||||
|
<li class="m-2">
|
||||||
|
<a href="${WEB_UI.FOOTER_CLI_URL}" target="_blank">
|
||||||
|
${translate('footerLinkCli')}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
if (WEB_UI.FOOTER_DMCA_URL != '') {
|
||||||
|
links.push(html`
|
||||||
|
<li class="m-2">
|
||||||
|
<a href="${WEB_UI.FOOTER_DMCA_URL}" target="_blank">
|
||||||
|
${translate('footerLinkDmca')}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
if (WEB_UI.FOOTER_SOURCE_URL != '') {
|
||||||
|
links.push(html`
|
||||||
|
<li class="m-2">
|
||||||
|
<a href="${WEB_UI.FOOTER_SOURCE_URL}" target="_blank">
|
||||||
|
${translate('footerLinkSource')}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
links.push(html`
|
||||||
|
<li class="m-2">
|
||||||
|
<a href="https://gitlab.com/timvisee/send" target="_blank">
|
||||||
|
${translate('footerLinkSource')}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<footer
|
<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"
|
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"
|
||||||
>
|
>
|
||||||
<div>${translate('footerText')}</div>
|
<ul
|
||||||
|
class="flex flex-col md:flex-row items-start md:items-center md:justify-start"
|
||||||
|
>
|
||||||
|
<li class="m-2">${translate('footerText')}</li>
|
||||||
|
</ul>
|
||||||
<ul
|
<ul
|
||||||
class="flex flex-col md:flex-row items-start md:items-center md:justify-end"
|
class="flex flex-col md:flex-row items-start md:items-center md:justify-end"
|
||||||
>
|
>
|
||||||
<li class="m-2">
|
${links}
|
||||||
<a href="https://gitlab.com/timvisee/send"
|
|
||||||
>${translate('footerLinkSource')}</a
|
|
||||||
>
|
|
||||||
</li>
|
|
||||||
</ul>
|
</ul>
|
||||||
</footer>
|
</footer>
|
||||||
`;
|
`;
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ class Header extends Component {
|
|||||||
this.account.render();
|
this.account.render();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
createElement() {
|
createElement() {
|
||||||
const title =
|
const title =
|
||||||
platform() === 'android'
|
platform() === 'android'
|
||||||
|
|||||||
@@ -100,7 +100,7 @@ module.exports = function(state, emit) {
|
|||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
case 'download':
|
case 'download':
|
||||||
emit('download', archive);
|
emit('download');
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
const html = require('choo/html');
|
const html = require('choo/html');
|
||||||
const assets = require('../../common/assets');
|
const assets = require('../../common/assets');
|
||||||
const { bytes } = require('../utils');
|
const { bytes } = require('../utils');
|
||||||
const { canceledSignup, submittedSignup } = require('../metrics');
|
|
||||||
|
|
||||||
module.exports = function(trigger) {
|
module.exports = function() {
|
||||||
return function(state, emit, close) {
|
return function(state, emit, close) {
|
||||||
const DAYS = Math.floor(state.LIMITS.MAX_EXPIRE_SECONDS / 86400);
|
const DAYS = Math.floor(state.LIMITS.MAX_EXPIRE_SECONDS / 86400);
|
||||||
let submitting = false;
|
let submitting = false;
|
||||||
@@ -72,7 +71,6 @@ module.exports = function(trigger) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function cancel(event) {
|
function cancel(event) {
|
||||||
canceledSignup({ trigger });
|
|
||||||
close(event);
|
close(event);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -85,7 +83,6 @@ module.exports = function(trigger) {
|
|||||||
|
|
||||||
const el = document.getElementById('email-input');
|
const el = document.getElementById('email-input');
|
||||||
const email = el.value;
|
const email = el.value;
|
||||||
submittedSignup({ trigger });
|
|
||||||
emit('login', emailish(email) ? email : null);
|
emit('login', emailish(email) ? email : null);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
23
app/user.js
23
app/user.js
@@ -109,27 +109,8 @@ export default class User {
|
|||||||
async startAuthFlow(trigger, utms = {}) {
|
async startAuthFlow(trigger, utms = {}) {
|
||||||
this.utms = utms;
|
this.utms = utms;
|
||||||
this.trigger = trigger;
|
this.trigger = trigger;
|
||||||
try {
|
this.flowId = null;
|
||||||
const params = new URLSearchParams({
|
this.flowBeginTime = null;
|
||||||
entrypoint: `send-${trigger}`,
|
|
||||||
form_type: 'email',
|
|
||||||
utm_source: utms.source || 'send',
|
|
||||||
utm_campaign: utms.campaign || 'none'
|
|
||||||
});
|
|
||||||
const res = await fetch(
|
|
||||||
`${this.authConfig.issuer}/metrics-flow?${params.toString()}`,
|
|
||||||
{
|
|
||||||
mode: 'cors'
|
|
||||||
}
|
|
||||||
);
|
|
||||||
const { flowId, flowBeginTime } = await res.json();
|
|
||||||
this.flowId = flowId;
|
|
||||||
this.flowBeginTime = flowBeginTime;
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e);
|
|
||||||
this.flowId = null;
|
|
||||||
this.flowBeginTime = null;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async login(email) {
|
async login(email) {
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ Of course, we don't want to expose the service on port 1443. Instead we want our
|
|||||||
* a2enmod proxy
|
* a2enmod proxy
|
||||||
* a2enmod proxy_http
|
* a2enmod proxy_http
|
||||||
* a2enmod proxy_wstunnel
|
* a2enmod proxy_wstunnel
|
||||||
|
* a2enmod rewrite
|
||||||
|
|
||||||
In your Apache virtual host configuration file, insert this:
|
In your Apache virtual host configuration file, insert this:
|
||||||
|
|
||||||
|
|||||||
@@ -12,18 +12,24 @@ Or run `docker build -t send:latest .` to create an image locally or `docker-com
|
|||||||
|
|
||||||
| Name | Description
|
| 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).
|
| `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_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.
|
| `REDIS_HOST` | Host name of the Redis server.
|
||||||
| `SENTRY_CLIENT` | Sentry Client ID
|
| `SENTRY_CLIENT` | Sentry Client ID
|
||||||
| `SENTRY_DSN` | Sentry DSN
|
| `SENTRY_DSN` | Sentry DSN
|
||||||
| `MAX_FILE_SIZE` | in bytes (defaults to 2147483648)
|
|
||||||
| `NODE_ENV` | "production"
|
|
||||||
| `BASE_URL` | The HTTPS URL where traffic will be served (e.g. `https://send.firefox.com`)
|
|
||||||
|
|
||||||
## Example:
|
## Example:
|
||||||
|
|
||||||
```sh
|
```bash
|
||||||
$ docker run --net=host -e 'NODE_ENV=production' \
|
$ docker run --net=host -e 'NODE_ENV=production' \
|
||||||
-e 'S3_BUCKET=testpilot-p2p-dev' \
|
-e 'S3_BUCKET=testpilot-p2p-dev' \
|
||||||
-e 'REDIS_HOST=dyf9s2r4vo3.bolxr4.0001.usw2.cache.amazonaws.com' \
|
-e 'REDIS_HOST=dyf9s2r4vo3.bolxr4.0001.usw2.cache.amazonaws.com' \
|
||||||
@@ -32,3 +38,9 @@ $ docker run --net=host -e 'NODE_ENV=production' \
|
|||||||
-e 'BASE_URL=https://send.firefox.com' \
|
-e 'BASE_URL=https://send.firefox.com' \
|
||||||
registry.gitlab.com/timvisee/send:latest
|
registry.gitlab.com/timvisee/send:latest
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Docker compose
|
||||||
|
|
||||||
|
For a Docker compose configuration example, see:
|
||||||
|
|
||||||
|
https://github.com/timvisee/send-docker-compose
|
||||||
|
|||||||
128
docs/metrics.md
128
docs/metrics.md
@@ -1,128 +0,0 @@
|
|||||||
# Send V2 Metrics Definitions
|
|
||||||
|
|
||||||
## Key Value Prop
|
|
||||||
|
|
||||||
Quickly and privately transfer large files from any device to any device.
|
|
||||||
|
|
||||||
## Key Business Question to Answer
|
|
||||||
|
|
||||||
Is the value proposition of a large encrypted file transfer service enough to drive Firefox Account relationships for non-Firefox users.
|
|
||||||
|
|
||||||
## Hypotheses to Test
|
|
||||||
|
|
||||||
### Primary - In support of Relationships KPI
|
|
||||||
|
|
||||||
We believe that a privacy-respecting file transfer service can drive Firefox Accounts beyond the Firefox Browser.
|
|
||||||
|
|
||||||
We will know this to be true when we see 250k Firefox Account creations from non-Firefox contexts w/in six months of launch.
|
|
||||||
|
|
||||||
### Secondary - In support of Revenue KPI
|
|
||||||
|
|
||||||
We believe that a privacy respecting service accessible beyond the reach of Firefox will provide a valuable platform to research, communicate with, and market to conscious choosers we have traditionally found hard to reach.
|
|
||||||
|
|
||||||
We will know this to be true when we can conduct six research tasks (surveys, A/B tests, fake doors, etc) in support of premium services KPIs in the first six months after launch.
|
|
||||||
|
|
||||||
## Overview of Key Measures
|
|
||||||
|
|
||||||
* Number of people using the service to send and receive files
|
|
||||||
* Why: measure of service size. Important for understanding addressable market size
|
|
||||||
* Percent of users who have or create an FxAccount via Send
|
|
||||||
* Why: representation of % of any service users who might be amenable to an upsell
|
|
||||||
* % of downloaders who convert into uploaders
|
|
||||||
* Why: represents a measure of our key growth-loop potential
|
|
||||||
* Count of uploads and size
|
|
||||||
* Why: Represents cost of service on a running basis
|
|
||||||
|
|
||||||
## Key Funnels
|
|
||||||
* App Open or Visit `--- DESIRED OUTCOME --->` Successful Upload
|
|
||||||
* Download UI Visit `--- DESIRED OUTCOME --->` Successful Download
|
|
||||||
* FxA UI Engagement `--- DESIRED OUTCOME --->` Authenticate
|
|
||||||
* **STRETCH** App Open or Visit `--- DESIRED OUTCOME --->` Successful Download
|
|
||||||
|
|
||||||
## Amplitude Schema
|
|
||||||
|
|
||||||
Please see, **See Amplitude HTTP API**(https://amplitude.zendesk.com/hc/en-us/articles/204771828) for HTTP API reference.
|
|
||||||
|
|
||||||
## Metric Events
|
|
||||||
|
|
||||||
In support of our KPIs we collect events from two separate contexts, server and client. The events are designed to have minimal correlation between contexts.
|
|
||||||
|
|
||||||
Server events collect lifecycle information about individual uploads but no user information; also time precision is truncated to hour increments. Client events collect information about how users interact with the UI but no upload identifiers.
|
|
||||||
|
|
||||||
### Server Events
|
|
||||||
|
|
||||||
Server events allow us to aggregate data about file lifecycle without collecting data about individual users. In this context `user_id` and `user_properties` describe the uploaded archive.
|
|
||||||
|
|
||||||
* `session_id` -1 (not part of a session)
|
|
||||||
* `user_id` hash of (archive_id + owner_id)
|
|
||||||
* `app_version` package.json version
|
|
||||||
* `time` timestamp truncated to hour precision
|
|
||||||
* `country`
|
|
||||||
* `region`
|
|
||||||
* `event_type` [server_upload | server_download | server_delete]
|
|
||||||
* `user_properties`
|
|
||||||
* `download_limit` set number of downloads
|
|
||||||
* `time_limit` set expiry duration
|
|
||||||
* `size` approximate size (log10)
|
|
||||||
* `anonymous` true if anonymous, false if fxa
|
|
||||||
* `event_properties`
|
|
||||||
* `download_count` downloads completed
|
|
||||||
* `ttl` time remaining before expiry truncated to hour
|
|
||||||
* `agent` the browser name or first 6 characters of the user agent that made the request
|
|
||||||
|
|
||||||
### Client Events
|
|
||||||
|
|
||||||
Client events allow us to aggregate data about how the user interface is being used without tracking the lifecycle of individual files. In this context `user_id` and `user_properties` describe the user. The `user_id` and `device_id` change for all users at the beginning of each month.
|
|
||||||
|
|
||||||
* `session_id` timestamp
|
|
||||||
* `user_id` hash of (fxa_id + Date.year + Date.month)
|
|
||||||
* `device_id` hash of (localStorage random id + Date.year + Date.month)
|
|
||||||
* `platform` [web | android]
|
|
||||||
* `country`
|
|
||||||
* `region`
|
|
||||||
* `language`
|
|
||||||
* `time` timestamp
|
|
||||||
* `os_name`
|
|
||||||
* `event_type` [client_visit | client_upload | client_download | client_delete | client_login | client_logout]
|
|
||||||
* `event_properties`
|
|
||||||
* `browser`
|
|
||||||
* `browser_version`
|
|
||||||
* `status` [ ok | error | cancel ]
|
|
||||||
* Event specific properties (see below)
|
|
||||||
* `user_properties`
|
|
||||||
* `active_count` number of active uploads
|
|
||||||
* `anonymous` true if anonymous, false if fxa
|
|
||||||
* `experiments` list of experiment ids the user is participating in
|
|
||||||
* `first_action` how this use came to Send the first time [ upload | download ]
|
|
||||||
|
|
||||||
#### Visit Event
|
|
||||||
|
|
||||||
* `entrypoint` [ upload | download ]
|
|
||||||
|
|
||||||
#### Upload Event
|
|
||||||
|
|
||||||
* `download_limit` download limit
|
|
||||||
* `file_count` number of files
|
|
||||||
* `password_protected` boolean
|
|
||||||
* `size` approximate size (log10)
|
|
||||||
* `time_limit` time limit
|
|
||||||
* `duration` approximate transfer duration (log10)
|
|
||||||
|
|
||||||
#### Download Event
|
|
||||||
|
|
||||||
* `password_protected` boolean
|
|
||||||
* `size` approximate size (log10)
|
|
||||||
* `duration` approximate transfer duration (log10)
|
|
||||||
|
|
||||||
#### Delete Event
|
|
||||||
|
|
||||||
* `age` hours since uploaded
|
|
||||||
* `downloaded` downloaded at least once
|
|
||||||
|
|
||||||
#### Login Event
|
|
||||||
|
|
||||||
* `trigger` [button | time | count | size]
|
|
||||||
|
|
||||||
#### Logout Event
|
|
||||||
|
|
||||||
* `trigger` [button | timeout]
|
|
||||||
2862
package-lock.json
generated
2862
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
32
package.json
32
package.json
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "send",
|
"name": "send",
|
||||||
"description": "File Sharing Experiment",
|
"description": "File Sharing Experiment",
|
||||||
"version": "3.4.0",
|
"version": "3.4.5",
|
||||||
"author": "Mozilla (https://mozilla.org)",
|
"author": "Mozilla (https://mozilla.org)",
|
||||||
"contributors": [
|
"contributors": [
|
||||||
"Tim Visee <3a4fb3964f@sinenomine.email> (https://timvisee.com)"
|
"Tim Visee <3a4fb3964f@sinenomine.email> (https://timvisee.com)"
|
||||||
@@ -61,24 +61,24 @@
|
|||||||
"cache": true
|
"cache": true
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^12.16.3"
|
"node": "^15.5.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "^7.12.10",
|
"@babel/core": "^7.13.10",
|
||||||
"@babel/plugin-proposal-class-properties": "^7.12.1",
|
"@babel/plugin-proposal-class-properties": "^7.13.0",
|
||||||
"@babel/plugin-syntax-dynamic-import": "^7.2.0",
|
"@babel/plugin-syntax-dynamic-import": "^7.2.0",
|
||||||
"@babel/preset-env": "^7.12.11",
|
"@babel/preset-env": "^7.13.10",
|
||||||
"@dannycoates/webcrypto-liner": "^0.1.37",
|
"@dannycoates/webcrypto-liner": "^0.1.37",
|
||||||
"@fullhuman/postcss-purgecss": "^1.3.0",
|
"@fullhuman/postcss-purgecss": "^1.3.0",
|
||||||
"@mattiasbuelens/web-streams-polyfill": "0.2.1",
|
"@mattiasbuelens/web-streams-polyfill": "0.2.1",
|
||||||
"@sentry/browser": "^5.29.2",
|
"@sentry/browser": "^5.30.0",
|
||||||
"asmcrypto.js": "^0.22.0",
|
"asmcrypto.js": "^0.22.0",
|
||||||
"babel-loader": "^8.2.2",
|
"babel-loader": "^8.2.2",
|
||||||
"babel-plugin-istanbul": "^5.2.0",
|
"babel-plugin-istanbul": "^5.2.0",
|
||||||
"base64-js": "^1.5.1",
|
"base64-js": "^1.5.1",
|
||||||
"content-disposition": "^0.5.3",
|
"content-disposition": "^0.5.3",
|
||||||
"copy-webpack-plugin": "^5.1.2",
|
"copy-webpack-plugin": "^5.1.2",
|
||||||
"core-js": "^3.8.2",
|
"core-js": "^3.9.1",
|
||||||
"crc": "^3.8.0",
|
"crc": "^3.8.0",
|
||||||
"cross-env": "^6.0.3",
|
"cross-env": "^6.0.3",
|
||||||
"css-loader": "^3.6.0",
|
"css-loader": "^3.6.0",
|
||||||
@@ -102,7 +102,7 @@
|
|||||||
"lint-staged": "^9.4.2",
|
"lint-staged": "^9.4.2",
|
||||||
"mocha": "^6.2.2",
|
"mocha": "^6.2.2",
|
||||||
"morgan": "^1.9.1",
|
"morgan": "^1.9.1",
|
||||||
"nanobus": "^4.4.0",
|
"nanobus": "^4.5.0",
|
||||||
"nanohtml": "^1.9.0",
|
"nanohtml": "^1.9.0",
|
||||||
"nanotiming": "^7.3.1",
|
"nanotiming": "^7.3.1",
|
||||||
"npm-run-all": "^4.1.5",
|
"npm-run-all": "^4.1.5",
|
||||||
@@ -113,22 +113,21 @@
|
|||||||
"proxyquire": "^2.1.3",
|
"proxyquire": "^2.1.3",
|
||||||
"puppeteer": "^2.0.0",
|
"puppeteer": "^2.0.0",
|
||||||
"raw-loader": "^3.1.0",
|
"raw-loader": "^3.1.0",
|
||||||
"redis-mock": "^0.47.0",
|
|
||||||
"rimraf": "^3.0.0",
|
"rimraf": "^3.0.0",
|
||||||
"script-loader": "^0.7.2",
|
"script-loader": "^0.7.2",
|
||||||
"sinon": "^7.5.0",
|
"sinon": "^7.5.0",
|
||||||
"string-hash": "^1.1.3",
|
"string-hash": "^1.1.3",
|
||||||
"stylelint": "^13.8.0",
|
"stylelint": "^13.12.0",
|
||||||
"stylelint-config-standard": "^19.0.0",
|
"stylelint-config-standard": "^19.0.0",
|
||||||
"stylelint-no-unsupported-browser-features": "^4.1.4",
|
"stylelint-no-unsupported-browser-features": "^4.1.4",
|
||||||
"svgo": "^1.3.2",
|
"svgo": "^1.3.2",
|
||||||
"svgo-loader": "^2.2.1",
|
"svgo-loader": "^2.2.2",
|
||||||
"tailwindcss": "^1.9.6",
|
"tailwindcss": "^1.9.6",
|
||||||
"val-loader": "^1.1.1",
|
"val-loader": "^1.1.1",
|
||||||
"webpack": "4.38.0",
|
"webpack": "4.38.0",
|
||||||
"webpack-cli": "^3.3.12",
|
"webpack-cli": "^3.3.12",
|
||||||
"webpack-dev-middleware": "^3.7.3",
|
"webpack-dev-middleware": "^3.7.3",
|
||||||
"webpack-dev-server": "^3.11.1",
|
"webpack-dev-server": "^3.11.2",
|
||||||
"webpack-manifest-plugin": "^2.2.0",
|
"webpack-manifest-plugin": "^2.2.0",
|
||||||
"webpack-unassert-loader": "^1.2.0"
|
"webpack-unassert-loader": "^1.2.0"
|
||||||
},
|
},
|
||||||
@@ -136,9 +135,9 @@
|
|||||||
"@dannycoates/express-ws": "^5.0.3",
|
"@dannycoates/express-ws": "^5.0.3",
|
||||||
"@fluent/bundle": "^0.13.0",
|
"@fluent/bundle": "^0.13.0",
|
||||||
"@fluent/langneg": "^0.3.0",
|
"@fluent/langneg": "^0.3.0",
|
||||||
"@google-cloud/storage": "^5.7.1",
|
"@google-cloud/storage": "^5.8.1",
|
||||||
"@sentry/node": "^5.29.2",
|
"@sentry/node": "^5.30.0",
|
||||||
"aws-sdk": "^2.824.0",
|
"aws-sdk": "^2.864.0",
|
||||||
"body-parser": "^1.19.0",
|
"body-parser": "^1.19.0",
|
||||||
"choo": "^7.0.0",
|
"choo": "^7.0.0",
|
||||||
"cldr-core": "^35.1.0",
|
"cldr-core": "^35.1.0",
|
||||||
@@ -150,8 +149,9 @@
|
|||||||
"mozlog": "^2.2.0",
|
"mozlog": "^2.2.0",
|
||||||
"node-fetch": "^2.6.1",
|
"node-fetch": "^2.6.1",
|
||||||
"redis": "^2.8.0",
|
"redis": "^2.8.0",
|
||||||
|
"redis-mock": "^0.47.0",
|
||||||
"selenium-standalone": "^6.23.0",
|
"selenium-standalone": "^6.23.0",
|
||||||
"ua-parser-js": "^0.7.23"
|
"ua-parser-js": "^0.7.24"
|
||||||
},
|
},
|
||||||
"availableLanguages": [
|
"availableLanguages": [
|
||||||
"en-US",
|
"en-US",
|
||||||
|
|||||||
@@ -29,6 +29,9 @@ updateFirefox = Update Firefox
|
|||||||
deletePopupCancel = Cancel
|
deletePopupCancel = Cancel
|
||||||
deleteButtonHover = Delete
|
deleteButtonHover = Delete
|
||||||
footerText = Not affiliated with Mozilla or Firefox.
|
footerText = Not affiliated with Mozilla or Firefox.
|
||||||
|
footerLinkDonate = Donate
|
||||||
|
footerLinkCli = CLI
|
||||||
|
footerLinkDmca = DMCA
|
||||||
footerLinkSource = Source
|
footerLinkSource = Source
|
||||||
passwordTryAgain = Incorrect password. Try again.
|
passwordTryAgain = Incorrect password. Try again.
|
||||||
javascriptRequired = Send requires JavaScript
|
javascriptRequired = Send requires JavaScript
|
||||||
|
|||||||
@@ -27,6 +27,9 @@ updateFirefox = Update Firefox
|
|||||||
deletePopupCancel = Cancel
|
deletePopupCancel = Cancel
|
||||||
deleteButtonHover = Delete
|
deleteButtonHover = Delete
|
||||||
footerText = Not affiliated with Mozilla or Firefox.
|
footerText = Not affiliated with Mozilla or Firefox.
|
||||||
|
footerLinkDonate = Donate
|
||||||
|
footerLinkCli = CLI
|
||||||
|
footerLinkDmca = DMCA
|
||||||
footerLinkSource = Source
|
footerLinkSource = Source
|
||||||
passwordTryAgain = Incorrect password. Try again.
|
passwordTryAgain = Incorrect password. Try again.
|
||||||
javascriptRequired = Send requires JavaScript
|
javascriptRequired = Send requires JavaScript
|
||||||
|
|||||||
@@ -29,6 +29,9 @@ updateFirefox = Firefox bijwerken
|
|||||||
deletePopupCancel = Annuleren
|
deletePopupCancel = Annuleren
|
||||||
deleteButtonHover = Verwijderen
|
deleteButtonHover = Verwijderen
|
||||||
footerText = Niet aangesloten aan Mozilla of Firefox.
|
footerText = Niet aangesloten aan Mozilla of Firefox.
|
||||||
|
footerLinkDonate = Doneren
|
||||||
|
footerLinkCli = CLI
|
||||||
|
footerLinkDmca = DMCA
|
||||||
footerLinkSource = Broncode
|
footerLinkSource = Broncode
|
||||||
passwordTryAgain = Onjuist wachtwoord. Probeer het opnieuw.
|
passwordTryAgain = Onjuist wachtwoord. Probeer het opnieuw.
|
||||||
javascriptRequired = Send vereist JavaScript
|
javascriptRequired = Send vereist JavaScript
|
||||||
|
|||||||
39
s3-lifecycle-example.xml
Normal file
39
s3-lifecycle-example.xml
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
<?xml version="1.0" ?>
|
||||||
|
<LifecycleConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
|
||||||
|
<!-- remove files for 1 day after 1 day -->
|
||||||
|
<Rule>
|
||||||
|
<ID>1skjet8gxudyans73v3p3d6hvq6yn3w02kq7931h81ohopd3</ID>
|
||||||
|
<Prefix>1-</Prefix>
|
||||||
|
<Status>Enabled</Status>
|
||||||
|
<Expiration>
|
||||||
|
<Days>1</Days>
|
||||||
|
</Expiration>
|
||||||
|
</Rule>
|
||||||
|
<!-- remove files for 7 days after 7 days -->
|
||||||
|
<Rule>
|
||||||
|
<ID>1skjet8gxudyans73v3p3d6hvq6yn3w02kq7931h81ohopd4</ID>
|
||||||
|
<Prefix>7-</Prefix>
|
||||||
|
<Status>Enabled</Status>
|
||||||
|
<Expiration>
|
||||||
|
<Days>7</Days>
|
||||||
|
</Expiration>
|
||||||
|
</Rule>
|
||||||
|
<!-- remove all files after 1 month -->
|
||||||
|
<Rule>
|
||||||
|
<ID>1skjet8gxudyans73v3p3d6hvq6yn3w02kq7931h81ohopd5</ID>
|
||||||
|
<Prefix/>
|
||||||
|
<Status>Enabled</Status>
|
||||||
|
<Expiration>
|
||||||
|
<Days>30</Days>
|
||||||
|
</Expiration>
|
||||||
|
</Rule>
|
||||||
|
<!-- remove aborted uploads after 1 day -->
|
||||||
|
<Rule>
|
||||||
|
<ID>1skjet8gxudyans73v3p3d6hvq6yn3w02kq7931h81ohopd6</ID>
|
||||||
|
<Prefix></Prefix>
|
||||||
|
<Status>Enabled</Status>
|
||||||
|
<AbortIncompleteMultipartUpload>
|
||||||
|
<DaysAfterInitiation>1</DaysAfterInitiation>
|
||||||
|
</AbortIncompleteMultipartUpload>
|
||||||
|
</Rule>
|
||||||
|
</LifecycleConfiguration>
|
||||||
@@ -1,171 +0,0 @@
|
|||||||
const crypto = require('crypto');
|
|
||||||
const fetch = require('node-fetch');
|
|
||||||
const config = require('./config');
|
|
||||||
const pkg = require('../package.json');
|
|
||||||
|
|
||||||
const HOUR = 1000 * 60 * 60;
|
|
||||||
|
|
||||||
function truncateToHour(timestamp) {
|
|
||||||
return Math.floor(timestamp / HOUR) * HOUR;
|
|
||||||
}
|
|
||||||
|
|
||||||
function orderOfMagnitude(n) {
|
|
||||||
return Math.floor(Math.log10(n));
|
|
||||||
}
|
|
||||||
|
|
||||||
function userId(fileId, ownerId) {
|
|
||||||
const hash = crypto.createHash('sha256');
|
|
||||||
hash.update(fileId);
|
|
||||||
hash.update(ownerId);
|
|
||||||
return hash.digest('hex').substring(32);
|
|
||||||
}
|
|
||||||
|
|
||||||
function statUploadEvent(data) {
|
|
||||||
const event = {
|
|
||||||
session_id: -1,
|
|
||||||
country: data.country,
|
|
||||||
region: data.state,
|
|
||||||
user_id: userId(data.id, data.owner),
|
|
||||||
app_version: pkg.version,
|
|
||||||
time: truncateToHour(Date.now()),
|
|
||||||
event_type: 'server_upload',
|
|
||||||
user_properties: {
|
|
||||||
download_limit: data.dlimit,
|
|
||||||
time_limit: data.timeLimit,
|
|
||||||
size: orderOfMagnitude(data.size),
|
|
||||||
anonymous: data.anonymous
|
|
||||||
},
|
|
||||||
event_properties: {
|
|
||||||
agent: data.agent
|
|
||||||
},
|
|
||||||
event_id: 0
|
|
||||||
};
|
|
||||||
return sendBatch([event]);
|
|
||||||
}
|
|
||||||
|
|
||||||
function statDownloadEvent(data) {
|
|
||||||
const event = {
|
|
||||||
session_id: -1,
|
|
||||||
country: data.country,
|
|
||||||
region: data.state,
|
|
||||||
user_id: userId(data.id, data.owner),
|
|
||||||
app_version: pkg.version,
|
|
||||||
time: truncateToHour(Date.now()),
|
|
||||||
event_type: 'server_download',
|
|
||||||
event_properties: {
|
|
||||||
agent: data.agent,
|
|
||||||
download_count: data.download_count,
|
|
||||||
ttl: data.ttl
|
|
||||||
},
|
|
||||||
event_id: data.download_count
|
|
||||||
};
|
|
||||||
return sendBatch([event]);
|
|
||||||
}
|
|
||||||
|
|
||||||
function statDeleteEvent(data) {
|
|
||||||
const event = {
|
|
||||||
session_id: -1,
|
|
||||||
country: data.country,
|
|
||||||
region: data.state,
|
|
||||||
user_id: userId(data.id, data.owner),
|
|
||||||
app_version: pkg.version,
|
|
||||||
time: truncateToHour(Date.now()),
|
|
||||||
event_type: 'server_delete',
|
|
||||||
event_properties: {
|
|
||||||
agent: data.agent,
|
|
||||||
download_count: data.download_count,
|
|
||||||
ttl: data.ttl
|
|
||||||
},
|
|
||||||
event_id: data.download_count + 1
|
|
||||||
};
|
|
||||||
return sendBatch([event]);
|
|
||||||
}
|
|
||||||
|
|
||||||
function clientEvent(
|
|
||||||
event,
|
|
||||||
ua,
|
|
||||||
language,
|
|
||||||
session_id,
|
|
||||||
deltaT,
|
|
||||||
platform,
|
|
||||||
country,
|
|
||||||
state
|
|
||||||
) {
|
|
||||||
const ep = event.event_properties || {};
|
|
||||||
const up = event.user_properties || {};
|
|
||||||
const event_properties = {
|
|
||||||
browser: ua.browser.name,
|
|
||||||
browser_version: ua.browser.version,
|
|
||||||
status: ep.status,
|
|
||||||
|
|
||||||
age: ep.age,
|
|
||||||
downloaded: ep.downloaded,
|
|
||||||
download_limit: ep.download_limit,
|
|
||||||
duration: ep.duration,
|
|
||||||
entrypoint: ep.entrypoint,
|
|
||||||
file_count: ep.file_count,
|
|
||||||
password_protected: ep.password_protected,
|
|
||||||
referrer: ep.referrer,
|
|
||||||
size: ep.size,
|
|
||||||
time_limit: ep.time_limit,
|
|
||||||
trigger: ep.trigger,
|
|
||||||
ttl: ep.ttl,
|
|
||||||
utm_campaign: ep.utm_campaign,
|
|
||||||
utm_content: ep.utm_content,
|
|
||||||
utm_medium: ep.utm_medium,
|
|
||||||
utm_source: ep.utm_source,
|
|
||||||
utm_term: ep.utm_term,
|
|
||||||
experiment: ep.experiment,
|
|
||||||
variant: ep.variant
|
|
||||||
};
|
|
||||||
const user_properties = {
|
|
||||||
active_count: up.active_count,
|
|
||||||
anonymous: up.anonymous,
|
|
||||||
experiments: up.experiments,
|
|
||||||
first_action: up.first_action
|
|
||||||
};
|
|
||||||
return {
|
|
||||||
app_version: pkg.version,
|
|
||||||
country: country,
|
|
||||||
device_id: event.device_id,
|
|
||||||
event_properties,
|
|
||||||
event_type: event.event_type,
|
|
||||||
language,
|
|
||||||
os_name: ua.os.name,
|
|
||||||
os_version: ua.os.version,
|
|
||||||
platform,
|
|
||||||
region: state,
|
|
||||||
session_id,
|
|
||||||
time: event.time + deltaT,
|
|
||||||
user_id: event.user_id,
|
|
||||||
user_properties
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
async function sendBatch(events, timeout = 1000) {
|
|
||||||
if (!config.amplitude_id) {
|
|
||||||
return 200;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
const result = await fetch('https://api.amplitude.com/batch', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify({
|
|
||||||
api_key: config.amplitude_id,
|
|
||||||
events
|
|
||||||
}),
|
|
||||||
timeout
|
|
||||||
});
|
|
||||||
return result.status;
|
|
||||||
} catch (e) {
|
|
||||||
return 500;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
statUploadEvent,
|
|
||||||
statDownloadEvent,
|
|
||||||
statDeleteEvent,
|
|
||||||
clientEvent,
|
|
||||||
sendBatch
|
|
||||||
};
|
|
||||||
@@ -13,6 +13,12 @@ module.exports = {
|
|||||||
MAX_FILES_PER_ARCHIVE: config.max_files_per_archive,
|
MAX_FILES_PER_ARCHIVE: config.max_files_per_archive,
|
||||||
MAX_ARCHIVES_PER_USER: config.max_archives_per_user
|
MAX_ARCHIVES_PER_USER: config.max_archives_per_user
|
||||||
},
|
},
|
||||||
|
WEB_UI: {
|
||||||
|
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
|
||||||
|
},
|
||||||
DEFAULTS: {
|
DEFAULTS: {
|
||||||
DOWNLOAD_COUNTS: config.download_counts,
|
DOWNLOAD_COUNTS: config.download_counts,
|
||||||
EXPIRE_TIMES_SECONDS: config.expire_times_seconds,
|
EXPIRE_TIMES_SECONDS: config.expire_times_seconds,
|
||||||
|
|||||||
@@ -100,16 +100,6 @@ const conf = convict({
|
|||||||
arg: 'port',
|
arg: 'port',
|
||||||
env: 'PORT'
|
env: 'PORT'
|
||||||
},
|
},
|
||||||
amplitude_id: {
|
|
||||||
format: String,
|
|
||||||
default: '',
|
|
||||||
env: 'AMPLITUDE_ID'
|
|
||||||
},
|
|
||||||
analytics_id: {
|
|
||||||
format: String,
|
|
||||||
default: '',
|
|
||||||
env: 'GOOGLE_ANALYTICS_ID'
|
|
||||||
},
|
|
||||||
sentry_id: {
|
sentry_id: {
|
||||||
format: String,
|
format: String,
|
||||||
default: '',
|
default: '',
|
||||||
@@ -194,6 +184,26 @@ const conf = convict({
|
|||||||
format: String,
|
format: String,
|
||||||
default: '',
|
default: '',
|
||||||
env: 'IP_DB'
|
env: 'IP_DB'
|
||||||
|
},
|
||||||
|
footer_donate_url: {
|
||||||
|
format: String,
|
||||||
|
default: '',
|
||||||
|
env: 'SEND_FOOTER_DONATE_URL'
|
||||||
|
},
|
||||||
|
footer_cli_url: {
|
||||||
|
format: String,
|
||||||
|
default: 'https://github.com/timvisee/ffsend',
|
||||||
|
env: 'SEND_FOOTER_CLI_URL'
|
||||||
|
},
|
||||||
|
footer_dmca_url: {
|
||||||
|
format: String,
|
||||||
|
default: '',
|
||||||
|
env: 'SEND_FOOTER_DMCA_URL'
|
||||||
|
},
|
||||||
|
footer_source_url: {
|
||||||
|
format: String,
|
||||||
|
default: 'https://github.com/timvisee/send',
|
||||||
|
env: 'SEND_FOOTER_SOURCE_URL'
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ module.exports = function(state) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var LIMITS = ${JSON.stringify(clientConstants.LIMITS)};
|
var LIMITS = ${JSON.stringify(clientConstants.LIMITS)};
|
||||||
|
var WEB_UI = ${JSON.stringify(clientConstants.WEB_UI)};
|
||||||
var DEFAULTS = ${JSON.stringify(clientConstants.DEFAULTS)};
|
var DEFAULTS = ${JSON.stringify(clientConstants.DEFAULTS)};
|
||||||
var PREFS = ${JSON.stringify(state.prefs)};
|
var PREFS = ${JSON.stringify(state.prefs)};
|
||||||
var downloadMetadata = ${
|
var downloadMetadata = ${
|
||||||
|
|||||||
@@ -1,23 +1,10 @@
|
|||||||
const storage = require('../storage');
|
const storage = require('../storage');
|
||||||
const { statDeleteEvent } = require('../amplitude');
|
|
||||||
|
|
||||||
module.exports = async function(req, res) {
|
module.exports = async function(req, res) {
|
||||||
try {
|
try {
|
||||||
const id = req.params.id;
|
const id = req.params.id;
|
||||||
const meta = req.meta;
|
|
||||||
const ttl = await storage.ttl(id);
|
|
||||||
await storage.del(id);
|
await storage.del(id);
|
||||||
res.sendStatus(200);
|
res.sendStatus(200);
|
||||||
statDeleteEvent({
|
|
||||||
id,
|
|
||||||
ip: req.ip,
|
|
||||||
country: req.geo.country,
|
|
||||||
state: req.geo.state,
|
|
||||||
owner: meta.owner,
|
|
||||||
download_count: meta.dl,
|
|
||||||
ttl,
|
|
||||||
agent: req.ua.browser.name || req.ua.ua.substring(0, 6)
|
|
||||||
});
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
res.sendStatus(404);
|
res.sendStatus(404);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
const storage = require('../storage');
|
const storage = require('../storage');
|
||||||
const mozlog = require('../log');
|
const mozlog = require('../log');
|
||||||
const log = mozlog('send.download');
|
const log = mozlog('send.download');
|
||||||
const { statDownloadEvent } = require('../amplitude');
|
|
||||||
|
|
||||||
module.exports = async function(req, res) {
|
module.exports = async function(req, res) {
|
||||||
const id = req.params.id;
|
const id = req.params.id;
|
||||||
@@ -27,17 +26,6 @@ module.exports = async function(req, res) {
|
|||||||
|
|
||||||
const dl = meta.dl + 1;
|
const dl = meta.dl + 1;
|
||||||
const dlimit = meta.dlimit;
|
const dlimit = meta.dlimit;
|
||||||
const ttl = await storage.ttl(id);
|
|
||||||
statDownloadEvent({
|
|
||||||
id,
|
|
||||||
ip: req.ip,
|
|
||||||
country: req.geo.country,
|
|
||||||
state: req.geo.state,
|
|
||||||
owner: meta.owner,
|
|
||||||
download_count: dl,
|
|
||||||
ttl,
|
|
||||||
agent: req.ua.browser.name || req.ua.ua.substring(0, 6)
|
|
||||||
});
|
|
||||||
try {
|
try {
|
||||||
if (dl >= dlimit) {
|
if (dl >= dlimit) {
|
||||||
await storage.del(id);
|
await storage.del(id);
|
||||||
|
|||||||
@@ -112,7 +112,6 @@ module.exports = function(app) {
|
|||||||
require('./params')
|
require('./params')
|
||||||
);
|
);
|
||||||
app.post(`/api/info/:id${ID_REGEX}`, auth.owner, require('./info'));
|
app.post(`/api/info/:id${ID_REGEX}`, auth.owner, require('./info'));
|
||||||
app.post('/api/metrics', require('./metrics'));
|
|
||||||
app.get('/__version__', function(req, res) {
|
app.get('/__version__', function(req, res) {
|
||||||
// eslint-disable-next-line node/no-missing-require
|
// eslint-disable-next-line node/no-missing-require
|
||||||
res.sendFile(require.resolve('../../dist/version.json'));
|
res.sendFile(require.resolve('../../dist/version.json'));
|
||||||
|
|||||||
@@ -1,24 +0,0 @@
|
|||||||
const { sendBatch, clientEvent } = require('../amplitude');
|
|
||||||
|
|
||||||
module.exports = async function(req, res) {
|
|
||||||
try {
|
|
||||||
const data = JSON.parse(req.body); // see http://crbug.com/490015
|
|
||||||
const deltaT = Date.now() - data.now;
|
|
||||||
const events = data.events.map(e =>
|
|
||||||
clientEvent(
|
|
||||||
e,
|
|
||||||
req.ua,
|
|
||||||
data.lang,
|
|
||||||
data.session_id + deltaT,
|
|
||||||
deltaT,
|
|
||||||
data.platform,
|
|
||||||
req.geo.country,
|
|
||||||
req.geo.state
|
|
||||||
)
|
|
||||||
);
|
|
||||||
const status = await sendBatch(events);
|
|
||||||
res.sendStatus(status);
|
|
||||||
} catch (e) {
|
|
||||||
res.sendStatus(500);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -4,7 +4,6 @@ const config = require('../config');
|
|||||||
const mozlog = require('../log');
|
const mozlog = require('../log');
|
||||||
const Limiter = require('../limiter');
|
const Limiter = require('../limiter');
|
||||||
const fxa = require('../fxa');
|
const fxa = require('../fxa');
|
||||||
const { statUploadEvent } = require('../amplitude');
|
|
||||||
const { encryptedSize } = require('../../app/utils');
|
const { encryptedSize } = require('../../app/utils');
|
||||||
|
|
||||||
const { Transform } = require('stream');
|
const { Transform } = require('stream');
|
||||||
@@ -108,18 +107,6 @@ module.exports = function(ws, req) {
|
|||||||
// in order to avoid having to check socket state and clean
|
// in order to avoid having to check socket state and clean
|
||||||
// up storage, possibly with an exception that we can catch.
|
// up storage, possibly with an exception that we can catch.
|
||||||
ws.send(JSON.stringify({ ok: true }));
|
ws.send(JSON.stringify({ ok: true }));
|
||||||
statUploadEvent({
|
|
||||||
id: newId,
|
|
||||||
ip: req.ip,
|
|
||||||
country: req.geo.country,
|
|
||||||
state: req.geo.state,
|
|
||||||
owner,
|
|
||||||
dlimit,
|
|
||||||
timeLimit,
|
|
||||||
anonymous: !user,
|
|
||||||
size: limiter.length,
|
|
||||||
agent: req.ua.browser.name || req.ua.ua.substring(0, 6)
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
log.error('upload', e);
|
log.error('upload', e);
|
||||||
|
|||||||
@@ -6,9 +6,9 @@ class S3Storage {
|
|||||||
this.log = log;
|
this.log = log;
|
||||||
const cfg = {};
|
const cfg = {};
|
||||||
if (config.s3_endpoint != '') {
|
if (config.s3_endpoint != '') {
|
||||||
cfg['endpoint'] = config.s3_endpoint;
|
cfg['endpoint'] = config.s3_endpoint;
|
||||||
}
|
}
|
||||||
cfg['s3ForcePathStyle'] = config.s3_use_path_style_endpoint
|
cfg['s3ForcePathStyle'] = config.s3_use_path_style_endpoint;
|
||||||
AWS.config.update(cfg);
|
AWS.config.update(cfg);
|
||||||
this.s3 = new AWS.S3();
|
this.s3 = new AWS.S3();
|
||||||
}
|
}
|
||||||
@@ -21,7 +21,9 @@ class S3Storage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getStream(id) {
|
getStream(id) {
|
||||||
return this.s3.getObject({ Bucket: this.bucket, Key: id }).createReadStream();
|
return this.s3
|
||||||
|
.getObject({ Bucket: this.bucket, Key: id })
|
||||||
|
.createReadStream();
|
||||||
}
|
}
|
||||||
|
|
||||||
set(id, file) {
|
set(id, file) {
|
||||||
|
|||||||
Reference in New Issue
Block a user