Compare commits

..

9 Commits

Author SHA1 Message Date
a552e4d766 Toasts (#45)
* locales

* add toasts and update deps

* changelog

* lock file
2022-07-20 10:41:37 +02:00
c3b1772728 Testing (#44)
* testing

* try playwrigth

* testing

* add pr support

* not on each commit

* add test ids

* make backend more configuratble

* 2.0.2

* spec
2022-07-19 21:55:05 +02:00
786878a3e4 Testing (#41)
* testing

* try playwrigth

* testing

* add pr support

* not on each commit
2022-07-19 14:12:51 +02:00
a5d98b76bd 2.0.1 (#40)
* locale from lokalise

* version bump

* update dependencies

* show size with overhead

* use base64 instead of hex and refactor a bit

* changelog & readme

* size limit

* locale

* add sync for svelte

* refarcor create & add loading animation

* changelog
2022-07-19 10:27:23 +02:00
9590c9b567 2 (#38)
* use redis

* update frontend and switch sanitize library

* changelog

* theming

* docker image

* documentation

* changelog

* clear up limit sizes

* version bump

* version bump
2022-07-16 14:16:54 +02:00
hash070
0913a8ad0c created and made a Chinese translation JSON file at ./frontend/locales/cn.json (#37) 2022-07-12 14:30:47 +02:00
d13c712e95 Update README.md 2022-07-08 10:45:40 +02:00
6230d2dbd0 Merge pull request #36 from Hash070/cn-translate
Completed the Chinese translation of README
2022-07-08 10:44:17 +02:00
hash070
dbfb383c73 Completed the Chinese translation of README 2022-07-08 16:18:12 +08:00
68 changed files with 1964 additions and 1087 deletions

View File

@@ -1,5 +1,4 @@
* *
!/entry.sh
!/backend/src !/backend/src
!/backend/Cargo.lock !/backend/Cargo.lock
@@ -13,3 +12,4 @@
!/frontend/pnpm-lock.yaml !/frontend/pnpm-lock.yaml
!/frontend/svelte.config.js !/frontend/svelte.config.js
!/frontend/tsconfig.json !/frontend/tsconfig.json
!/frontend/vite.config.js

1
.gitattributes vendored
View File

@@ -1 +1,2 @@
*.afdesign filter=lfs diff=lfs merge=lfs -text *.afdesign filter=lfs diff=lfs merge=lfs -text
test/assets/** filter=lfs diff=lfs merge=lfs -text

36
.github/workflows/test.yaml vendored Normal file
View File

@@ -0,0 +1,36 @@
on:
pull_request:
jobs:
test:
runs-on: ubuntu-latest
services:
redis:
image: redis:7-alpine
ports:
- 6379:6379
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
with:
node-version: "16"
- uses: pnpm/action-setup@v2
with:
version: 7
- uses: actions-rs/toolchain@v1
with:
toolchain: 1.61
- name: Prepare
run: |
pnpm install
pnpm run ci:prepare
- name: Install Playwright
run: npx playwright install --with-deps
- name: Run your tests
run: pnpm run test
- name: Upload test results
if: always()
uses: actions/upload-artifact@v2
with:
name: playwright-report
path: playwright-report

3
.gitignore vendored
View File

@@ -9,3 +9,6 @@ node_modules
/build /build
/functions /functions
.env .env
General
test-results

View File

@@ -5,6 +5,49 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [2.0.2] - 2022-07-20
### Added
- Toasts for events.
- E2E Tests.
- Make backend more configurable
## [2.0.1] - 2022-07-18
### Added
- Max file size on the client now.
- Loading information.
### Changed
- Changed encoding from hex to base64.
- Chinese language code.
- Notable speed improvements for big files.
## [2.0.0] - 2022-07-16
### Added
- Theming for logo and description text.
### Changed
- Moved to redis.
- New html sanitizing library.
## [2.0.0-rc.0] - 2022-07-15
### Added
- Theming for logo and description text.
### Changed
- Moved to redis.
- New html sanitizing library.
## [1.5.3] - 2022-06-07 ## [1.5.3] - 2022-06-07
### Changed ### Changed

View File

@@ -1,9 +1,10 @@
# FRONTEND # FRONTEND
FROM node:16-alpine as client FROM node:16-alpine as client
WORKDIR /tmp WORKDIR /tmp
RUN npm install -g pnpm RUN npm install -g pnpm@7
COPY ./frontend ./ COPY ./frontend ./
RUN pnpm install RUN pnpm install
RUN pnpm exec svelte-kit sync
RUN pnpm run build RUN pnpm run build
@@ -11,6 +12,8 @@ RUN pnpm run build
FROM rust:1.61-alpine as backend FROM rust:1.61-alpine as backend
WORKDIR /tmp WORKDIR /tmp
RUN apk add libc-dev openssl-dev alpine-sdk RUN apk add libc-dev openssl-dev alpine-sdk
COPY ./backend/Cargo.* ./
RUN cargo fetch
COPY ./backend ./ COPY ./backend ./
RUN cargo build --release RUN cargo build --release
@@ -18,9 +21,9 @@ RUN cargo build --release
# RUNNER # RUNNER
FROM alpine FROM alpine
WORKDIR /app WORKDIR /app
COPY ./entry.sh .
COPY --from=backend /tmp/target/release/cryptgeon . COPY --from=backend /tmp/target/release/cryptgeon .
COPY --from=client /tmp/build ./frontend/build COPY --from=client /tmp/build ./frontend
ENV MEMCACHE=memcached:11211 ENV FRONTEND_PATH="./frontend"
ENV REDIS="redis://redis/"
EXPOSE 5000 EXPOSE 5000
ENTRYPOINT [ "/app/entry.sh" ] ENTRYPOINT [ "/app/cryptgeon" ]

View File

@@ -9,10 +9,12 @@
<img alt="Latest version" src="https://img.shields.io/github/v/release/cupcakearmy/cryptgeon?style=for-the-badge" /> <img alt="Latest version" src="https://img.shields.io/github/v/release/cupcakearmy/cryptgeon?style=for-the-badge" />
</a> </a>
<br/> <br/><br/>
<a href="https://www.producthunt.com/posts/cryptgeon?utm_source=badge-featured&utm_medium=badge&utm_souce=badge-cryptgeon" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/featured.svg?post_id=295189&theme=light" alt="Cryptgeon - Securely share self-destructing notes | Product Hunt" height="50" /></a> <a href="https://www.producthunt.com/posts/cryptgeon?utm_source=badge-featured&utm_medium=badge&utm_souce=badge-cryptgeon" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/featured.svg?post_id=295189&theme=light" alt="Cryptgeon - Securely share self-destructing notes | Product Hunt" height="50" /></a>
<a href=""><img src="./.github/lokalise.png" height="50"> <a href=""><img src="./.github/lokalise.png" height="50">
<br/> <br/><br/>
EN | [简体中文](README_zh-CN.md)
## About? ## About?
@@ -49,12 +51,14 @@ of the notes even if it tried to.
## Environment Variables ## Environment Variables
| Variable | Default | Description | | Variable | Default | Description |
| ---------------- | ----------------- | --------------------------------------------------------------------------------------- | | ---------------- | ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `MEMCACHE` | `memcached:11211` | Memcached URL to connect to. | | `REDIS` | `redis://redis/` | Redis URL to connect to. |
| `SIZE_LIMIT` | `1 KiB` | Max size for body. Accepted values according to [byte-unit](https://docs.rs/byte-unit/) | | `SIZE_LIMIT` | `1 KiB` | Max size for body. Accepted values according to [byte-unit](https://docs.rs/byte-unit/). <br> `512 MiB` is the maximum allowed. <br> The frontend will show that number including the ~35% encoding overhead. |
| `MAX_VIEWS` | `100` | Maximal number of views. | | `MAX_VIEWS` | `100` | Maximal number of views. |
| `MAX_EXPIRATION` | `360` | Maximal expiration in minutes. | | `MAX_EXPIRATION` | `360` | Maximal expiration in minutes. |
| `ALLOW_ADVANCED` | `true` | Allow custom configuration. If set to `false` all notes will be one view only. | | `ALLOW_ADVANCED` | `true` | Allow custom configuration. If set to `false` all notes will be one view only. |
| `THEME_IMAGE` | `""` | Custom image for replacing the logo. Must be publicly reachable |
| `THEME_TEXT` | `""` | Custom text for replacing the description below the logo |
## Deployment ## Deployment
@@ -67,19 +71,18 @@ Docker is the easiest way. There is the [official image here](https://hub.docker
```yaml ```yaml
# docker-compose.yml # docker-compose.yml
version: '3.7' version: '3.8'
services: services:
memcached: redis:
image: memcached:1-alpine image: redis:7-alpine
entrypoint: memcached -m 128M -I 4M # Limit to 128 MB Ram, 4M per entry, customize at free will.
app: app:
image: cupcakearmy/cryptgeon:latest image: cupcakearmy/cryptgeon:latest
depends_on: depends_on:
- memcached - redis
environment: environment:
SIZE_LIMIT: 4M SIZE_LIMIT: 4 MiB
ports: ports:
- 80:5000 - 80:5000
``` ```
@@ -105,16 +108,15 @@ networks:
external: true external: true
services: services:
memcached: redis:
image: memcached:1-alpine image: redis:7-alpine
restart: unless-stopped restart: unless-stopped
entrypoint: memcached -m 128M -I 4M # Limit to 128 MB Ram, 4M per entry, customize at free will.
app: app:
image: cupcakearmy/cryptgeon:latest image: cupcakearmy/cryptgeon:latest
restart: unless-stopped restart: unless-stopped
depends_on: depends_on:
- memcached - redis
networks: networks:
- default - default
- proxy - proxy
@@ -130,7 +132,7 @@ services:
**Requirements** **Requirements**
- `pnpm`: `>=6` - `pnpm`: `>=6`
- `node`: `>=14` - `node`: `>=16`
- `rust`: edition `2021` - `rust`: edition `2021`
**Install** **Install**
@@ -157,12 +159,31 @@ pnpm run dev
Running `pnpm run dev` in the root folder will start the following things: Running `pnpm run dev` in the root folder will start the following things:
- a memcache docker container - redis docker container
- rust backend with hot reload - rust backend
- client with hot reload - client
You can see the app under [localhost:1234](http://localhost:1234). You can see the app under [localhost:1234](http://localhost:1234).
## Tests
Tests are end to end tests written with Playwright.
```sh
pnpm run ci:prepare
docker compose up redis -d
pnpm run ci:server
# In another terminal.
# Use the test or test:local script. The local version only runs in one browser for quicker development.
pnpm run test:local
```
###### Attributions ###### Attributions
Icons made by <a href="https://www.freepik.com" title="Freepik">freepik</a> from <a href="https://www.flaticon.com/" title="Flaticon">www.flaticon.com</a> - Test data:
- Text for tests [Nietzsche Ipsum](https://nietzsche-ipsum.com/)
- [AES Paper](https://www.cs.miami.edu/home/burt/learning/Csc688.012/rijndael/rijndael_doc_V2.pdf)
- [Unsplash Pictures](https://unsplash.com/)
- Loading animation by [Nikhil Krishnan](https://codepen.io/nikhil8krishnan/pen/rVoXJa)
- Icons made by <a href="https://www.freepik.com" title="Freepik">freepik</a> from <a href="https://www.flaticon.com/" title="Flaticon">www.flaticon.com</a>

166
README_zh-CN.md Normal file
View File

@@ -0,0 +1,166 @@
<p align="center">
<img src="./design/Github_zh-CN.png" alt="logo">
</p>
<a href="https://discord.gg/nuby6RnxZt">
<img alt="discord" src="https://img.shields.io/discord/252403122348097536?style=for-the-badge" />
<img alt="docker pulls" src="https://img.shields.io/docker/pulls/cupcakearmy/cryptgeon?style=for-the-badge" />
<img alt="Docker image size badge" src="https://img.shields.io/docker/image-size/cupcakearmy/cryptgeon?style=for-the-badge" />
<img alt="Latest version" src="https://img.shields.io/github/v/release/cupcakearmy/cryptgeon?style=for-the-badge" />
</a>
<br/>
<a href="https://www.producthunt.com/posts/cryptgeon?utm_source=badge-featured&utm_medium=badge&utm_souce=badge-cryptgeon" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/featured.svg?post_id=295189&theme=light" alt="Cryptgeon - Securely share self-destructing notes | Product Hunt" height="50" /></a>
<a href=""><img src="./.github/lokalise.png" height="50">
<br/>
[EN](README.md) | 简体中文
## 关于本项目
_加密鸽_ 是一个受 [_PrivNote_](https://privnote.com)项目启发的安全、开源共享密信和文件共享服务器
> 🌍 如果你想翻译此项目请随时与我联系.
>
> 感谢 [Lokalise](https://lokalise.com/) 提供免费的平台服务支持
## 演示示例
查看加密鸽的在线演示 demo https://cryptgeon.nicco.io.
## 功能
- 服务端无法解密和查看客户端加密的内容
- 查看次数或时间限制,阅后即焚
- 您发送的数据将存放于内存中,不会写入到磁盘中
- 黑暗模式支持
## 加密鸽是如何工作的?
加密鸽会为每条笔记都生成一个独立的 <code>id (256bit)</code><code>key 256(bit)</code>
其中<code>id</code>用于保存和提取密信, 在这之后这封密信将会被客户端使用 AES 算法的 GCM 模式和`key`进行加密然后发送至服务器,数据将会保存在服务器的内存中且永远不会被持久化到硬盘上,服务端永远不会得到密钥并且无法解读密信的内容。
## 屏幕截图
![screenshot](./design/Screens.png)
## 环境变量
| 变量名称 | 默认值 | 描述 |
| ----------------- | ---------------- | --------------------------------------------------------------------------------- |
| `REDIS` | `redis://redis/` | Redis URL to connect to. |
| `SIZE_LIMIT` | `1 KiB` | 最大请求体(body)限制。有关支持的数值请查看 [字节单位](https://docs.rs/byte-unit/) |
| `MAX_VIEWS` | `100` | 密信最多查看次数限制 |
| ` MAX_EXPIRATION` | `360` | 密信最长过期时间限制(分钟) |
| `ALLOW_ADVANCED` | `true` | 是否允许自定义设置,该项如果设为`false`,则不会显示自定义设置模块 |
## 部署
加密鸽必须使用`https`,否则浏览器可能将不会支援加密鸽的加密算法。
### Docker
Docker 是最简单的部署方式。这里是[官方镜像的地址](https://hub.docker.com/r/cupcakearmy/cryptgeon)。
附:译者的[部署笔记](https://www.hash070.top/archives/cryptgeon-docker-deploy.html)
```yaml
# docker-compose.yml
version: '3.8'
services:
redis:
image: redis:7-alpine
app:
image: cupcakearmy/cryptgeon:latest
depends_on:
- redis
environment:
SIZE_LIMIT: 4 MiB
ports:
- 80:5000
```
### NGINX 反向代理
查看 [examples/nginx](https://github.com/cupcakearmy/cryptgeon/tree/main/examples/nginx) 目录。那里有几个示例反代配置文件模板,其中一个是带 https 配置的反代配置模板,你需要指定服务器的名称和证书才能生效。
### Traefik 2
假设:
- 外部 Docker 代理网络 `proxy`
- 证书解析器 `le`
- 一个 https 入站点 `secure`
- 域名 `example.org`
```yaml
version: '3.8'
networks:
proxy:
external: true
services:
redis:
image: redis:7-alpine
restart: unless-stopped
app:
image: cupcakearmy/cryptgeon:latest
restart: unless-stopped
depends_on:
- redis
networks:
- default
- proxy
labels:
- traefik.enable=true
- traefik.http.routers.cryptgeon.rule=Host(`example.org`)
- traefik.http.routers.cryptgeon.entrypoints=secure
- traefik.http.routers.cryptgeon.tls.certresolver=le
```
## 开发
**环境要求**
- `pnpm`: `>=6`
- `node`: `>=14`
- `rust`: edition `2021`
**安装**
```bash
pnpm install
pnpm --prefix frontend install
# Also you need cargo watch if you don't already have it installed.
# https://lib.rs/crates/cargo-watch
cargo install cargo-watch
```
**运行**
确保你的 Docker 正在运行
> If you are on `macOS` you might need to disable AirPlay Receiver as it uses port 5000 (So stupid...)
> https://developer.apple.com/forums/thread/682332
```bash
pnpm run dev
```
在根目录执行 `pnpm run dev` 会开启下列服务:
- 一个 redis docker 容器
- 无热重载的 rust 后端
- 可热重载的客户端
你可以通过 1234 端口进入该应用,即 [localhost:1234](http://localhost:1234).
###### Attributions
本项目所使用的图标由<a href="https://www.flaticon.com/" title="Flaticon">www.flaticon.com 的<a href="https://www.freepik.com" title="Freepik">freepik</a>制作</a>

436
backend/Cargo.lock generated
View File

@@ -21,9 +21,9 @@ dependencies = [
[[package]] [[package]]
name = "actix-files" name = "actix-files"
version = "0.6.0" version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d81bde9a79336aa51ebed236e91fc1a0528ff67cfdf4f68ca4c61ede9fd26fb5" checksum = "e04dcf7654254676d434b0285e2298d577ed4826f67f536e7a39bb0f64721164"
dependencies = [ dependencies = [
"actix-http", "actix-http",
"actix-service", "actix-service",
@@ -44,9 +44,9 @@ dependencies = [
[[package]] [[package]]
name = "actix-http" name = "actix-http"
version = "3.0.4" version = "3.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a5885cb81a0d4d0d322864bea1bb6c2a8144626b4fdc625d4c51eba197e7797a" checksum = "6f9ffb6db08c1c3a1f4aef540f1a63193adc73c4fbd40b75a95fc8c5258f6e51"
dependencies = [ dependencies = [
"actix-codec", "actix-codec",
"actix-rt", "actix-rt",
@@ -66,16 +66,16 @@ dependencies = [
"http", "http",
"httparse", "httparse",
"httpdate", "httpdate",
"itoa", "itoa 1.0.2",
"language-tags", "language-tags",
"local-channel", "local-channel",
"log",
"mime", "mime",
"percent-encoding", "percent-encoding",
"pin-project-lite", "pin-project-lite",
"rand", "rand",
"sha-1", "sha1 0.10.1",
"smallvec", "smallvec",
"tracing",
"zstd", "zstd",
] ]
@@ -154,9 +154,9 @@ dependencies = [
[[package]] [[package]]
name = "actix-web" name = "actix-web"
version = "4.0.1" version = "4.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f4e5ebffd51d50df56a3ae0de0e59487340ca456f05dd0b90c0a7a6dd6a74d31" checksum = "a27e8fe9ba4ae613c21f677c2cfaf0696c3744030c6f485b34634e502d6bb379"
dependencies = [ dependencies = [
"actix-codec", "actix-codec",
"actix-http", "actix-http",
@@ -176,7 +176,7 @@ dependencies = [
"encoding_rs", "encoding_rs",
"futures-core", "futures-core",
"futures-util", "futures-util",
"itoa", "itoa 1.0.2",
"language-tags", "language-tags",
"log", "log",
"mime", "mime",
@@ -194,9 +194,9 @@ dependencies = [
[[package]] [[package]]
name = "actix-web-codegen" name = "actix-web-codegen"
version = "4.0.0" version = "4.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7525bedf54704abb1d469e88d7e7e9226df73778798a69cea5022d53b2ae91bc" checksum = "5f270541caec49c15673b0af0e9a00143421ad4f118d2df7edcb68b627632f56"
dependencies = [ dependencies = [
"actix-router", "actix-router",
"proc-macro2", "proc-macro2",
@@ -251,6 +251,28 @@ version = "0.10.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "619743e34b5ba4e9703bba34deac3427c72507c7159f5fd030aea8cac0cfe341" checksum = "619743e34b5ba4e9703bba34deac3427c72507c7159f5fd030aea8cac0cfe341"
[[package]]
name = "async-trait"
version = "0.1.56"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "96cf8829f67d2eab0b2dfa42c5d0ef737e0724e4a82b01b3e292456202b19716"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "atty"
version = "0.2.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8"
dependencies = [
"hermit-abi",
"libc",
"winapi",
]
[[package]] [[package]]
name = "autocfg" name = "autocfg"
version = "1.1.0" version = "1.1.0"
@@ -325,12 +347,6 @@ dependencies = [
"utf8-width", "utf8-width",
] ]
[[package]]
name = "byteorder"
version = "1.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610"
[[package]] [[package]]
name = "bytes" name = "bytes"
version = "1.1.0" version = "1.1.0"
@@ -339,9 +355,9 @@ checksum = "c4872d67bab6358e59559027aa3b9157c53d9358c51423c17554809a8858e0f8"
[[package]] [[package]]
name = "bytestring" name = "bytestring"
version = "1.0.0" version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "90706ba19e97b90786e19dc0d5e2abd80008d99d4c0c5d1ad0b5e72cec7c494d" checksum = "86b6a75fd3048808ef06af5cd79712be8111960adaf89d90250974b38fc3928a"
dependencies = [ dependencies = [
"bytes", "bytes",
] ]
@@ -361,6 +377,16 @@ version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "combine"
version = "4.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2a604e93b79d1808327a6fca85a6f2d69de66461e7620f5a4cbf5fb4d1d7c948"
dependencies = [
"bytes",
"memchr",
]
[[package]] [[package]]
name = "convert_case" name = "convert_case"
version = "0.4.0" version = "0.4.0"
@@ -398,16 +424,17 @@ dependencies = [
[[package]] [[package]]
name = "cryptgeon" name = "cryptgeon"
version = "1.5.3" version = "2.0.2"
dependencies = [ dependencies = [
"actix-files", "actix-files",
"actix-web", "actix-web",
"bs62", "bs62",
"byte-unit", "byte-unit",
"dotenv", "dotenv",
"env_logger",
"lazy_static", "lazy_static",
"memcache",
"mime", "mime",
"redis",
"ring", "ring",
"serde", "serde",
"serde_json", "serde_json",
@@ -415,9 +442,9 @@ dependencies = [
[[package]] [[package]]
name = "crypto-common" name = "crypto-common"
version = "0.1.3" version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "57952ca27b5e3606ff4dd79b0020231aaf9d6aa76dc05fd30137538c50bd3ce8" checksum = "5999502d32b9c48d492abe66392408144895020ec4709e549e840799f3bb74c0"
dependencies = [ dependencies = [
"generic-array", "generic-array",
"typenum", "typenum",
@@ -452,6 +479,12 @@ version = "0.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77c90badedccf4105eca100756a0b1289e191f6fcbdadd3cee1d2f614f97da8f" checksum = "77c90badedccf4105eca100756a0b1289e191f6fcbdadd3cee1d2f614f97da8f"
[[package]]
name = "dtoa"
version = "0.4.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56899898ce76aaf4a0f24d914c97ea6ed976d42fec6ad33fcbb0a1103e07b2b0"
[[package]] [[package]]
name = "encoding_rs" name = "encoding_rs"
version = "0.8.31" version = "0.8.31"
@@ -462,15 +495,16 @@ dependencies = [
] ]
[[package]] [[package]]
name = "enum_dispatch" name = "env_logger"
version = "0.3.8" version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0eb359f1476bf611266ac1f5355bc14aeca37b299d0ebccc038ee7058891c9cb" checksum = "0b2cf0344971ee6c64c31be0d530793fba457d322dfec2810c453d0ef228f9c3"
dependencies = [ dependencies = [
"once_cell", "atty",
"proc-macro2", "humantime",
"quote", "log",
"syn", "regex",
"termcolor",
] ]
[[package]] [[package]]
@@ -495,21 +529,6 @@ version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
[[package]]
name = "foreign-types"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1"
dependencies = [
"foreign-types-shared",
]
[[package]]
name = "foreign-types-shared"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"
[[package]] [[package]]
name = "form_urlencoded" name = "form_urlencoded"
version = "1.0.1" version = "1.0.1"
@@ -562,13 +581,13 @@ dependencies = [
[[package]] [[package]]
name = "getrandom" name = "getrandom"
version = "0.2.6" version = "0.2.7"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9be70c98951c83b8d2f8f60d7065fa6d5146873094452a1008da8c2f1e4205ad" checksum = "4eb1a864a501629691edf6c15a593b7a51eebaa1e8468e9ddc623de7c9b58ec6"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"libc", "libc",
"wasi 0.10.2+wasi-snapshot-preview1", "wasi",
] ]
[[package]] [[package]]
@@ -592,9 +611,9 @@ dependencies = [
[[package]] [[package]]
name = "hashbrown" name = "hashbrown"
version = "0.11.2" version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e" checksum = "db0d4cf898abf0081f964436dc980e96670a0f36863e4b83aaacdb65c9d7ccc3"
[[package]] [[package]]
name = "hermit-abi" name = "hermit-abi"
@@ -613,7 +632,7 @@ checksum = "75f43d41e26995c17e71ee126451dd3941010b0514a81a9d11f3b341debc2399"
dependencies = [ dependencies = [
"bytes", "bytes",
"fnv", "fnv",
"itoa", "itoa 1.0.2",
] ]
[[package]] [[package]]
@@ -634,6 +653,12 @@ version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421" checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421"
[[package]]
name = "humantime"
version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4"
[[package]] [[package]]
name = "idna" name = "idna"
version = "0.2.3" version = "0.2.3"
@@ -647,22 +672,19 @@ dependencies = [
[[package]] [[package]]
name = "indexmap" name = "indexmap"
version = "1.8.2" version = "1.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6012d540c5baa3589337a98ce73408de9b5a25ec9fc2c6fd6be8f0d39e0ca5a" checksum = "10a35a97730320ffe8e2d410b5d3b69279b98d2c14bdb8b70ea89ecf7888d41e"
dependencies = [ dependencies = [
"autocfg", "autocfg",
"hashbrown", "hashbrown",
] ]
[[package]] [[package]]
name = "instant" name = "itoa"
version = "0.1.12" version = "0.4.8"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" checksum = "b71991ff56294aa922b450139ee08b3bfc70982c6b2c7562771375cf73542dd4"
dependencies = [
"cfg-if",
]
[[package]] [[package]]
name = "itoa" name = "itoa"
@@ -681,9 +703,9 @@ dependencies = [
[[package]] [[package]]
name = "js-sys" name = "js-sys"
version = "0.3.57" version = "0.3.58"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "671a26f820db17c2a2750743f1dd03bafd15b98c9f30c7c2628c024c05d73397" checksum = "c3fac17f7123a73ca62df411b1bf727ccc805daa070338fda671c86dac1bdc27"
dependencies = [ dependencies = [
"wasm-bindgen", "wasm-bindgen",
] ]
@@ -749,20 +771,6 @@ version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a3e378b66a060d48947b590737b30a1be76706c8dd7b8ba0f2fe3989c68a853f" checksum = "a3e378b66a060d48947b590737b30a1be76706c8dd7b8ba0f2fe3989c68a853f"
[[package]]
name = "memcache"
version = "0.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "757b4de02817c63ff4efc98a282b305977a7468b2bdf2085c140cdab1604fa61"
dependencies = [
"byteorder",
"enum_dispatch",
"openssl",
"r2d2",
"rand",
"url",
]
[[package]] [[package]]
name = "memchr" name = "memchr"
version = "2.5.0" version = "2.5.0"
@@ -796,13 +804,13 @@ dependencies = [
[[package]] [[package]]
name = "mio" name = "mio"
version = "0.8.3" version = "0.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "713d550d9b44d89174e066b7a6217ae06234c10cb47819a88290d2b353c31799" checksum = "57ee1c23c7c63b0c9250c339ffdc69255f110b298b901b9f6c82547b7b87caaf"
dependencies = [ dependencies = [
"libc", "libc",
"log", "log",
"wasi 0.11.0+wasi-snapshot-preview1", "wasi",
"windows-sys", "windows-sys",
] ]
@@ -857,59 +865,9 @@ dependencies = [
[[package]] [[package]]
name = "once_cell" name = "once_cell"
version = "1.12.0" version = "1.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7709cef83f0c1f58f666e746a08b21e0085f7440fa6a29cc194d68aac97a4225" checksum = "18a6dbe30758c9f83eb00cbea4ac95966305f5a7772f3f42ebfc7fc7eddbd8e1"
[[package]]
name = "openssl"
version = "0.10.40"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fb81a6430ac911acb25fe5ac8f1d2af1b4ea8a4fdfda0f1ee4292af2e2d8eb0e"
dependencies = [
"bitflags",
"cfg-if",
"foreign-types",
"libc",
"once_cell",
"openssl-macros",
"openssl-sys",
]
[[package]]
name = "openssl-macros"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b501e44f11665960c7e7fcf062c7d96a14ade4aa98116c004b2e37b5be7d736c"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "openssl-sys"
version = "0.9.74"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "835363342df5fba8354c5b453325b110ffd54044e588c539cf2f20a8014e4cb1"
dependencies = [
"autocfg",
"cc",
"libc",
"pkg-config",
"vcpkg",
]
[[package]]
name = "parking_lot"
version = "0.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7d17b78036a60663b797adeaee46f5c9dfebb86948d1255007a1d6be0271ff99"
dependencies = [
"instant",
"lock_api",
"parking_lot_core 0.8.5",
]
[[package]] [[package]]
name = "parking_lot" name = "parking_lot"
@@ -918,21 +876,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f"
dependencies = [ dependencies = [
"lock_api", "lock_api",
"parking_lot_core 0.9.3", "parking_lot_core",
]
[[package]]
name = "parking_lot_core"
version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d76e8e1493bcac0d2766c42737f34458f1c8c50c0d23bcb24ea953affb273216"
dependencies = [
"cfg-if",
"instant",
"libc",
"redox_syscall",
"smallvec",
"winapi",
] ]
[[package]] [[package]]
@@ -972,12 +916,6 @@ version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
[[package]]
name = "pkg-config"
version = "0.3.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1df8c4ec4b0627e53bdf214615ad287367e482558cf84b109250b37464dc03ae"
[[package]] [[package]]
name = "ppv-lite86" name = "ppv-lite86"
version = "0.2.16" version = "0.2.16"
@@ -986,33 +924,22 @@ checksum = "eb9f9e6e233e5c4a35559a617bf40a4ec447db2e84c20b55a6f83167b7e57872"
[[package]] [[package]]
name = "proc-macro2" name = "proc-macro2"
version = "1.0.39" version = "1.0.40"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c54b25569025b7fc9651de43004ae593a75ad88543b17178aa5e1b9c4f15f56f" checksum = "dd96a1e8ed2596c337f8eae5f24924ec83f5ad5ab21ea8e455d3566c69fbcaf7"
dependencies = [ dependencies = [
"unicode-ident", "unicode-ident",
] ]
[[package]] [[package]]
name = "quote" name = "quote"
version = "1.0.18" version = "1.0.20"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1feb54ed693b93a84e14094943b84b7c4eae204c512b7ccb95ab0c66d278ad1" checksum = "3bcdf212e9776fbcb2d23ab029360416bb1706b1aea2d1a5ba002727cbcab804"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
] ]
[[package]]
name = "r2d2"
version = "0.8.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "545c5bc2b880973c9c10e4067418407a0ccaa3091781d1671d46eb35107cb26f"
dependencies = [
"log",
"parking_lot 0.11.2",
"scheduled-thread-pool",
]
[[package]] [[package]]
name = "rand" name = "rand"
version = "0.8.5" version = "0.8.5"
@@ -1043,6 +970,21 @@ dependencies = [
"getrandom", "getrandom",
] ]
[[package]]
name = "redis"
version = "0.21.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1a80b5f38d7f5a020856a0e16e40a9cfabf88ae8f0e4c2dcd8a3114c1e470852"
dependencies = [
"async-trait",
"combine",
"dtoa",
"itoa 0.4.8",
"percent-encoding",
"sha1 0.6.1",
"url",
]
[[package]] [[package]]
name = "redox_syscall" name = "redox_syscall"
version = "0.2.13" version = "0.2.13"
@@ -1054,9 +996,9 @@ dependencies = [
[[package]] [[package]]
name = "regex" name = "regex"
version = "1.5.6" version = "1.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d83f127d94bdbcda4c8cc2e50f6f84f4b611f69c902699ca385a39c3a75f9ff1" checksum = "4c4eb3267174b8c6c2f654116623910a0fef09c4753f8dd83db29c48a0df988b"
dependencies = [ dependencies = [
"aho-corasick", "aho-corasick",
"memchr", "memchr",
@@ -1065,9 +1007,9 @@ dependencies = [
[[package]] [[package]]
name = "regex-syntax" name = "regex-syntax"
version = "0.6.26" version = "0.6.27"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49b3de9ec5dc0a3417da371aab17d729997c15010e7fd24ff707773a33bddb64" checksum = "a3f87b73ce11b1619a3c6332f45341e0047173771e8b8b73f87bfeefb7b56244"
[[package]] [[package]]
name = "ring" name = "ring"
@@ -1099,15 +1041,6 @@ version = "1.0.10"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f3f6f92acf49d1b98f7a81226834412ada05458b7364277387724a237f062695" checksum = "f3f6f92acf49d1b98f7a81226834412ada05458b7364277387724a237f062695"
[[package]]
name = "scheduled-thread-pool"
version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "977a7519bff143a44f842fd07e80ad1329295bd71686457f18e496736f4bf9bf"
dependencies = [
"parking_lot 0.12.1",
]
[[package]] [[package]]
name = "scopeguard" name = "scopeguard"
version = "1.1.0" version = "1.1.0"
@@ -1116,24 +1049,24 @@ checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd"
[[package]] [[package]]
name = "semver" name = "semver"
version = "1.0.9" version = "1.0.12"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8cb243bdfdb5936c8dc3c45762a19d12ab4550cdc753bc247637d4ec35a040fd" checksum = "a2333e6df6d6598f2b1974829f853c2b4c5f4a6e503c10af918081aa6f8564e1"
[[package]] [[package]]
name = "serde" name = "serde"
version = "1.0.137" version = "1.0.138"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "61ea8d54c77f8315140a05f4c7237403bf38b72704d031543aa1d16abbf517d1" checksum = "1578c6245786b9d168c5447eeacfb96856573ca56c9d68fdcf394be134882a47"
dependencies = [ dependencies = [
"serde_derive", "serde_derive",
] ]
[[package]] [[package]]
name = "serde_derive" name = "serde_derive"
version = "1.0.137" version = "1.0.138"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1f26faba0c3959972377d3b2d306ee9f71faee9714294e41bb777f83f88578be" checksum = "023e9b1467aef8a10fb88f25611870ada9800ef7e22afce356bb0d2387b6f27c"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@@ -1142,11 +1075,11 @@ dependencies = [
[[package]] [[package]]
name = "serde_json" name = "serde_json"
version = "1.0.81" version = "1.0.82"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b7ce2b32a1aed03c558dc61a5cd328f15aff2dbc17daad8fb8af04d2100e15c" checksum = "82c2c1fdcd807d1098552c5b9a36e425e42e9fbd7c6a37a8425f390f781f7fa7"
dependencies = [ dependencies = [
"itoa", "itoa 1.0.2",
"ryu", "ryu",
"serde", "serde",
] ]
@@ -1158,22 +1091,37 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd"
dependencies = [ dependencies = [
"form_urlencoded", "form_urlencoded",
"itoa", "itoa 1.0.2",
"ryu", "ryu",
"serde", "serde",
] ]
[[package]] [[package]]
name = "sha-1" name = "sha1"
version = "0.10.0" version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "028f48d513f9678cda28f6e4064755b3fbb2af6acd672f2c209b62323f7aea0f" checksum = "c1da05c97445caa12d05e848c4a4fcbbea29e748ac28f7e80e9b010392063770"
dependencies = [
"sha1_smol",
]
[[package]]
name = "sha1"
version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c77f4e7f65455545c2153c1253d25056825e77ee2533f0e41deb65a93a34852f"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"cpufeatures", "cpufeatures",
"digest", "digest",
] ]
[[package]]
name = "sha1_smol"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae1a47186c03a32177042e55dbc5fd5aee900b8e0069a8d70fba96a9375cd012"
[[package]] [[package]]
name = "signal-hook-registry" name = "signal-hook-registry"
version = "1.4.0" version = "1.4.0"
@@ -1191,9 +1139,9 @@ checksum = "eb703cfe953bccee95685111adeedb76fabe4e97549a58d16f03ea7b9367bb32"
[[package]] [[package]]
name = "smallvec" name = "smallvec"
version = "1.8.0" version = "1.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f2dd574626839106c320a323308629dcb1acfc96e32a8cba364ddc61ac23ee83" checksum = "2fd0db749597d91ff862fd1d55ea87f7855a744a8425a64695b6fca237d1dad1"
[[package]] [[package]]
name = "socket2" name = "socket2"
@@ -1213,9 +1161,9 @@ checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d"
[[package]] [[package]]
name = "syn" name = "syn"
version = "1.0.96" version = "1.0.98"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0748dd251e24453cb8717f0354206b91557e4ec8703673a4b30208f2abaf1ebf" checksum = "c50aef8a904de4c23c788f104b7dddc7d6f79c647c7c8ce4cc8f73eb0ca773dd"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@@ -1223,12 +1171,21 @@ dependencies = [
] ]
[[package]] [[package]]
name = "time" name = "termcolor"
version = "0.3.9" version = "1.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2702e08a7a860f005826c6815dcac101b19b5eb330c27fe4a5928fec1d20ddd" checksum = "bab24d30b911b2376f3a13cc2cd443142f0c81dda04c118693e35b3835757755"
dependencies = [ dependencies = [
"itoa", "winapi-util",
]
[[package]]
name = "time"
version = "0.3.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72c91f41dcb2f096c05f0873d667dceec1087ce5bcf984ec8ffb19acddbb3217"
dependencies = [
"itoa 1.0.2",
"libc", "libc",
"num_threads", "num_threads",
"time-macros", "time-macros",
@@ -1266,7 +1223,7 @@ dependencies = [
"memchr", "memchr",
"mio", "mio",
"once_cell", "once_cell",
"parking_lot 0.12.1", "parking_lot",
"pin-project-lite", "pin-project-lite",
"signal-hook-registry", "signal-hook-registry",
"socket2", "socket2",
@@ -1289,9 +1246,9 @@ dependencies = [
[[package]] [[package]]
name = "tracing" name = "tracing"
version = "0.1.34" version = "0.1.35"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5d0ecdcb44a79f0fe9844f0c4f33a342cbcbb5117de8001e6ba0dc2351327d09" checksum = "a400e31aa60b9d44a52a8ee0343b5b18566b03a8321e0d321f695cf56e940160"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"log", "log",
@@ -1301,11 +1258,11 @@ dependencies = [
[[package]] [[package]]
name = "tracing-core" name = "tracing-core"
version = "0.1.26" version = "0.1.28"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f54c8ca710e81886d498c2fd3331b56c93aa248d49de2222ad2742247c60072f" checksum = "7b7358be39f2f274f322d2aaed611acc57f382e8eb1e5b48cb9ae30933495ce7"
dependencies = [ dependencies = [
"lazy_static", "once_cell",
] ]
[[package]] [[package]]
@@ -1331,15 +1288,15 @@ checksum = "099b7128301d285f79ddd55b9a83d5e6b9e97c92e0ea0daebee7263e932de992"
[[package]] [[package]]
name = "unicode-ident" name = "unicode-ident"
version = "1.0.0" version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d22af068fba1eb5edcb4aea19d382b2a3deb4c8f9d475c589b6ada9e0fd493ee" checksum = "5bd2fe26506023ed7b5e1e315add59d6f584c621d037f9368fea9cfb988f368c"
[[package]] [[package]]
name = "unicode-normalization" name = "unicode-normalization"
version = "0.1.19" version = "0.1.21"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d54590932941a9e9266f0832deed84ebe1bf2e4c9e4a3554d393d18f5e854bf9" checksum = "854cbdc4f7bc6ae19c820d44abdc3277ac3e1b2b93db20a636825d9322fb60e6"
dependencies = [ dependencies = [
"tinyvec", "tinyvec",
] ]
@@ -1368,24 +1325,12 @@ version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5190c9442dcdaf0ddd50f37420417d219ae5261bbf5db120d0f9bab996c9cba1" checksum = "5190c9442dcdaf0ddd50f37420417d219ae5261bbf5db120d0f9bab996c9cba1"
[[package]]
name = "vcpkg"
version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
[[package]] [[package]]
name = "version_check" name = "version_check"
version = "0.9.4" version = "0.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
[[package]]
name = "wasi"
version = "0.10.2+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6"
[[package]] [[package]]
name = "wasi" name = "wasi"
version = "0.11.0+wasi-snapshot-preview1" version = "0.11.0+wasi-snapshot-preview1"
@@ -1394,9 +1339,9 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
[[package]] [[package]]
name = "wasm-bindgen" name = "wasm-bindgen"
version = "0.2.80" version = "0.2.81"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "27370197c907c55e3f1a9fbe26f44e937fe6451368324e009cba39e139dc08ad" checksum = "7c53b543413a17a202f4be280a7e5c62a1c69345f5de525ee64f8cfdbc954994"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"wasm-bindgen-macro", "wasm-bindgen-macro",
@@ -1404,9 +1349,9 @@ dependencies = [
[[package]] [[package]]
name = "wasm-bindgen-backend" name = "wasm-bindgen-backend"
version = "0.2.80" version = "0.2.81"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "53e04185bfa3a779273da532f5025e33398409573f348985af9a1cbf3774d3f4" checksum = "5491a68ab4500fa6b4d726bd67408630c3dbe9c4fe7bda16d5c82a1fd8c7340a"
dependencies = [ dependencies = [
"bumpalo", "bumpalo",
"lazy_static", "lazy_static",
@@ -1419,9 +1364,9 @@ dependencies = [
[[package]] [[package]]
name = "wasm-bindgen-macro" name = "wasm-bindgen-macro"
version = "0.2.80" version = "0.2.81"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "17cae7ff784d7e83a2fe7611cfe766ecf034111b49deb850a3dc7699c08251f5" checksum = "c441e177922bc58f1e12c022624b6216378e5febc2f0533e41ba443d505b80aa"
dependencies = [ dependencies = [
"quote", "quote",
"wasm-bindgen-macro-support", "wasm-bindgen-macro-support",
@@ -1429,9 +1374,9 @@ dependencies = [
[[package]] [[package]]
name = "wasm-bindgen-macro-support" name = "wasm-bindgen-macro-support"
version = "0.2.80" version = "0.2.81"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "99ec0dc7a4756fffc231aab1b9f2f578d23cd391390ab27f952ae0c9b3ece20b" checksum = "7d94ac45fcf608c1f45ef53e748d35660f168490c10b23704c7779ab8f5c3048"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@@ -1442,15 +1387,15 @@ dependencies = [
[[package]] [[package]]
name = "wasm-bindgen-shared" name = "wasm-bindgen-shared"
version = "0.2.80" version = "0.2.81"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d554b7f530dee5964d9a9468d95c1f8b8acae4f282807e7d27d4b03099a46744" checksum = "6a89911bd99e5f3659ec4acf9c4d93b0a90fe4a2a11f15328472058edc5261be"
[[package]] [[package]]
name = "web-sys" name = "web-sys"
version = "0.3.57" version = "0.3.58"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7b17e741662c70c8bd24ac5c5b18de314a2c26c32bf8346ee1e6f53de919c283" checksum = "2fed94beee57daf8dd7d51f2b15dc2bcde92d7a72304cdf662a4371008b71b90"
dependencies = [ dependencies = [
"js-sys", "js-sys",
"wasm-bindgen", "wasm-bindgen",
@@ -1472,6 +1417,15 @@ version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
[[package]]
name = "winapi-util"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178"
dependencies = [
"winapi",
]
[[package]] [[package]]
name = "winapi-x86_64-pc-windows-gnu" name = "winapi-x86_64-pc-windows-gnu"
version = "0.4.0" version = "0.4.0"
@@ -1523,18 +1477,18 @@ checksum = "c811ca4a8c853ef420abd8592ba53ddbbac90410fab6903b3e79972a631f7680"
[[package]] [[package]]
name = "zstd" name = "zstd"
version = "0.10.2+zstd.1.5.2" version = "0.11.2+zstd.1.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5f4a6bd64f22b5e3e94b4e238669ff9f10815c27a5180108b849d24174a83847" checksum = "20cc960326ece64f010d2d2107537f26dc589a6573a316bd5b1dba685fa5fde4"
dependencies = [ dependencies = [
"zstd-safe", "zstd-safe",
] ]
[[package]] [[package]]
name = "zstd-safe" name = "zstd-safe"
version = "4.1.6+zstd.1.5.2" version = "5.0.2+zstd.1.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94b61c51bb270702d6167b8ce67340d2754b088d0c091b06e593aa772c3ee9bb" checksum = "1d2a5585e04f9eea4b2a3d1eca508c4dee9592a89ef6f450c11719da0726f4db"
dependencies = [ dependencies = [
"libc", "libc",
"zstd-sys", "zstd-sys",
@@ -1542,9 +1496,9 @@ dependencies = [
[[package]] [[package]]
name = "zstd-sys" name = "zstd-sys"
version = "1.6.3+zstd.1.5.2" version = "2.0.1+zstd.1.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc49afa5c8d634e75761feda8c592051e7eeb4683ba827211eb0d731d3402ea8" checksum = "9fd07cbbc53846d9145dbffdf6dd09a7a0aa52be46741825f5c97bdd4f73f12b"
dependencies = [ dependencies = [
"cc", "cc",
"libc", "libc",

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "cryptgeon" name = "cryptgeon"
version = "1.5.3" version = "2.0.2"
authors = ["cupcakearmy <hi@nicco.io>"] authors = ["cupcakearmy <hi@nicco.io>"]
edition = "2021" edition = "2021"
@@ -18,7 +18,8 @@ serde_json = "1"
lazy_static = "1" lazy_static = "1"
ring = "0.16" ring = "0.16"
bs62 = "0.1" bs62 = "0.1"
memcache = "0.16"
byte-unit = "4" byte-unit = "4"
dotenv = "0.15" dotenv = "0.15"
mime = "0.3" mime = "0.3"
env_logger = "0.9"
redis = "0.21.5"

View File

@@ -1,14 +1,17 @@
use actix_files::{Files, NamedFile}; use actix_files::{Files, NamedFile};
use actix_web::{web, Result}; use actix_web::{web, Result};
use crate::config;
pub fn init(cfg: &mut web::ServiceConfig) { pub fn init(cfg: &mut web::ServiceConfig) {
cfg.service( cfg.service(
Files::new("/", "./frontend/build") Files::new("/", config::FRONTEND_PATH.to_string())
.index_file("index.html") .index_file("index.html")
.use_etag(true), .use_etag(true),
); );
} }
pub async fn index() -> Result<NamedFile> { pub async fn index() -> Result<NamedFile> {
Ok(NamedFile::open("./frontend/build/index.html")?) let index = format!("{}{}", config::FRONTEND_PATH.to_string(), "/index.html");
Ok(NamedFile::open(index)?)
} }

View File

@@ -1,13 +1,22 @@
use byte_unit::Byte; use byte_unit::Byte;
// Internal
lazy_static! { lazy_static! {
pub static ref VERSION: String = option_env!("CARGO_PKG_VERSION") pub static ref VERSION: String = option_env!("CARGO_PKG_VERSION")
.unwrap_or("Unknown") .unwrap_or("Unknown")
.to_string(); .to_string();
pub static ref LIMIT: u32 = pub static ref FRONTEND_PATH: String =
std::env::var("FRONTEND_PATH").unwrap_or("../frontend/build".to_string());
pub static ref LISTEN_ADDR: String =
std::env::var("LISTEN_ADDR").unwrap_or("0.0.0.0:5000".to_string());
}
// CONFIG
lazy_static! {
pub static ref LIMIT: usize =
Byte::from_str(std::env::var("SIZE_LIMIT").unwrap_or("1 KiB".to_string())) Byte::from_str(std::env::var("SIZE_LIMIT").unwrap_or("1 KiB".to_string()))
.unwrap() .unwrap()
.get_bytes() as u32; .get_bytes() as usize;
pub static ref MAX_VIEWS: u32 = std::env::var("MAX_VIEWS") pub static ref MAX_VIEWS: u32 = std::env::var("MAX_VIEWS")
.unwrap_or("100".to_string()) .unwrap_or("100".to_string())
.parse() .parse()
@@ -21,3 +30,15 @@ lazy_static! {
.parse() .parse()
.unwrap(); .unwrap();
} }
// THEME
lazy_static! {
pub static ref THEME_IMAGE: String = std::env::var("THEME_IMAGE")
.unwrap_or("".to_string())
.parse()
.unwrap();
pub static ref THEME_TEXT: String = std::env::var("THEME_TEXT")
.unwrap_or("".to_string())
.parse()
.unwrap();
}

View File

@@ -1,4 +1,7 @@
use actix_web::{middleware, web, App, HttpServer}; use actix_web::{
middleware::{self, Logger},
web, App, HttpServer,
};
use dotenv::dotenv; use dotenv::dotenv;
#[macro_use] #[macro_use]
@@ -15,8 +18,10 @@ mod store;
#[actix_web::main] #[actix_web::main]
async fn main() -> std::io::Result<()> { async fn main() -> std::io::Result<()> {
dotenv().ok(); dotenv().ok();
env_logger::init_from_env(env_logger::Env::new().default_filter_or("warning"));
return HttpServer::new(|| { return HttpServer::new(|| {
App::new() App::new()
.wrap(Logger::new("%a \"%r\" %s %b %T"))
.wrap(middleware::Compress::default()) .wrap(middleware::Compress::default())
.wrap(middleware::DefaultHeaders::default()) .wrap(middleware::DefaultHeaders::default())
.configure(size::init) .configure(size::init)
@@ -24,7 +29,7 @@ async fn main() -> std::io::Result<()> {
.configure(client::init) .configure(client::init)
.default_service(web::to(client::index)) .default_service(web::to(client::index))
}) })
.bind("0.0.0.0:5000")? .bind(config::LISTEN_ADDR.to_string())?
.run() .run()
.await; .await;
} }

View File

@@ -22,9 +22,11 @@ struct NotePath {
async fn one(path: web::Path<NotePath>) -> impl Responder { async fn one(path: web::Path<NotePath>) -> impl Responder {
let p = path.into_inner(); let p = path.into_inner();
let note = store::get(&p.id); let note = store::get(&p.id);
match note { match note {
None => return HttpResponse::NotFound().finish(), Ok(Some(_)) => HttpResponse::Ok().json(NoteInfo {}),
Some(_) => return HttpResponse::Ok().json(NoteInfo {}), Ok(None) => HttpResponse::NotFound().finish(),
Err(e) => HttpResponse::InternalServerError().body(e.to_string()),
} }
} }
@@ -64,8 +66,10 @@ async fn create(note: web::Json<Note>) -> impl Responder {
} }
_ => {} _ => {}
} }
store::set(&id.clone(), &n.clone()); match store::set(&id.clone(), &n.clone()) {
return HttpResponse::Ok().json(CreateResponse { id: id }); Ok(_) => return HttpResponse::Ok().json(CreateResponse { id: id }),
Err(e) => return HttpResponse::InternalServerError().body(e.to_string()),
}
} }
#[delete("/{id}")] #[delete("/{id}")]
@@ -73,8 +77,9 @@ async fn delete(path: web::Path<NotePath>) -> impl Responder {
let p = path.into_inner(); let p = path.into_inner();
let note = store::get(&p.id); let note = store::get(&p.id);
match note { match note {
None => return HttpResponse::NotFound().finish(), Err(e) => HttpResponse::InternalServerError().body(e.to_string()),
Some(note) => { Ok(None) => return HttpResponse::NotFound().finish(),
Ok(Some(note)) => {
let mut changed = note.clone(); let mut changed = note.clone();
if changed.views == None && changed.expiration == None { if changed.views == None && changed.expiration == None {
return HttpResponse::BadRequest().finish(); return HttpResponse::BadRequest().finish();
@@ -84,9 +89,19 @@ async fn delete(path: web::Path<NotePath>) -> impl Responder {
changed.views = Some(v - 1); changed.views = Some(v - 1);
let id = p.id.clone(); let id = p.id.clone();
if v <= 1 { if v <= 1 {
store::del(&id); match store::del(&id) {
Err(e) => {
return HttpResponse::InternalServerError().body(e.to_string())
}
_ => {}
}
} else { } else {
store::set(&id, &changed.clone()); match store::set(&id, &changed.clone()) {
Err(e) => {
return HttpResponse::InternalServerError().body(e.to_string())
}
_ => {}
}
} }
} }
_ => {} _ => {}
@@ -96,8 +111,12 @@ async fn delete(path: web::Path<NotePath>) -> impl Responder {
match changed.expiration { match changed.expiration {
Some(e) => { Some(e) => {
if e < n { if e < n {
store::del(&p.id.clone()); match store::del(&p.id.clone()) {
return HttpResponse::BadRequest().finish(); Ok(_) => return HttpResponse::BadRequest().finish(),
Err(e) => {
return HttpResponse::InternalServerError().body(e.to_string())
}
}
} }
} }
_ => {} _ => {}

View File

@@ -1,18 +1,11 @@
use crate::config;
use actix_web::web; use actix_web::web;
use byte_unit::Byte;
use mime; use mime;
lazy_static! {
pub static ref LIMIT: usize =
Byte::from_str(std::env::var("SIZE_LIMIT").unwrap_or("1 KiB".to_string()))
.unwrap()
.get_bytes() as usize;
}
pub fn init(cfg: &mut web::ServiceConfig) { pub fn init(cfg: &mut web::ServiceConfig) {
let json = web::JsonConfig::default().limit(*LIMIT); let json = web::JsonConfig::default().limit(*config::LIMIT);
let plain = web::PayloadConfig::default() let plain = web::PayloadConfig::default()
.limit(*LIMIT) .limit(*config::LIMIT)
.mimetype(mime::STAR_STAR); .mimetype(mime::STAR_STAR);
cfg.app_data(json).app_data(plain); cfg.app_data(json).app_data(plain);
} }

View File

@@ -2,9 +2,14 @@ use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize)] #[derive(Serialize, Deserialize)]
pub struct Status { pub struct Status {
// General
pub version: String, pub version: String,
// Config
pub max_size: u32, pub max_size: u32,
pub max_views: u32, pub max_views: u32,
pub max_expiration: u32, pub max_expiration: u32,
pub allow_advanced: bool, pub allow_advanced: bool,
// Theme
pub theme_image: String,
pub theme_text: String,
} }

View File

@@ -7,10 +7,12 @@ use crate::status::Status;
async fn get_status() -> impl Responder { async fn get_status() -> impl Responder {
return HttpResponse::Ok().json(Status { return HttpResponse::Ok().json(Status {
version: config::VERSION.to_string(), version: config::VERSION.to_string(),
max_size: *config::LIMIT, max_size: *config::LIMIT as u32,
max_views: *config::MAX_VIEWS, max_views: *config::MAX_VIEWS,
max_expiration: *config::MAX_EXPIRATION, max_expiration: *config::MAX_EXPIRATION,
allow_advanced: *config::ALLOW_ADVANCED, allow_advanced: *config::ALLOW_ADVANCED,
theme_image: config::THEME_IMAGE.to_string(),
theme_text: config::THEME_TEXT.to_string(),
}); });
} }

View File

@@ -1,36 +1,55 @@
use memcache; use redis;
use redis::Commands;
use crate::note::now; use crate::note::now;
use crate::note::Note; use crate::note::Note;
lazy_static! { lazy_static! {
static ref CLIENT: memcache::Client = memcache::connect(format!( static ref REDIS_CLIENT: String = std::env::var("REDIS")
"memcache://{}?timeout=10&tcp_nodelay=true", .unwrap_or("redis://127.0.0.1/".to_string())
std::env::var("MEMCACHE").unwrap_or("127.0.0.1:11211".to_string()) .parse()
))
.unwrap(); .unwrap();
} }
pub fn set(id: &String, note: &Note) { fn get_connection() -> Result<redis::Connection, &'static str> {
let serialized = serde_json::to_string(&note.clone()).unwrap(); let client =
let expiration: u32 = match note.expiration { redis::Client::open(REDIS_CLIENT.to_string()).map_err(|_| "Unable to connect to redis")?;
Some(e) => e - now(), client
None => 0, .get_connection()
}; .map_err(|_| "Unable to connect to redis")
CLIENT.set(id, serialized, expiration).unwrap();
} }
pub fn get(id: &String) -> Option<Note> { pub fn set(id: &String, note: &Note) -> Result<(), &'static str> {
let value: Option<String> = CLIENT.get(&id).unwrap(); let serialized = serde_json::to_string(&note.clone()).unwrap();
let mut conn = get_connection()?;
conn.set(id, serialized)
.map_err(|_| "Unable to set note in redis")?;
match note.expiration {
Some(e) => {
let seconds = e - now();
conn.expire(id, seconds as usize)
.map_err(|_| "Unable to set expiration on notion")?
}
None => {}
};
Ok(())
}
pub fn get(id: &String) -> Result<Option<Note>, &'static str> {
let mut conn = get_connection()?;
let value: Option<String> = conn.get(id).map_err(|_| "Could not load note in redis")?;
match value { match value {
None => return None, None => return Ok(None),
Some(s) => { Some(s) => {
let deserialize: Note = serde_json::from_str(&s).unwrap(); let deserialize: Note = serde_json::from_str(&s).unwrap();
return Some(deserialize); return Ok(Some(deserialize));
} }
} }
} }
pub fn del(id: &String) { pub fn del(id: &String) -> Result<(), &'static str> {
CLIENT.delete(id).unwrap(); let mut conn = get_connection()?;
conn.del(id).map_err(|_| "Unable to delete note in redis")?;
Ok(())
} }

BIN
design/Github_zh-CN.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

View File

@@ -1,21 +1,19 @@
# DEV Compose file. # DEV Compose file.
# For a production file see: README.md # For a production file see: README.md
version: '3.7' version: '3.8'
services: services:
memcached: redis:
image: memcached:1-alpine image: redis:7-alpine
restart: unless-stopped
entrypoint: memcached -m 256M -I 128M
ports: ports:
- 11211:11211 - 6379:6379
app: app:
build: . build: .
depends_on: depends_on:
- memcached - redis
environment: environment:
SIZE_LIMIT: 128M SIZE_LIMIT: 128 MiB
ports: ports:
- 80:5000 - 1234:5000

View File

@@ -1,13 +0,0 @@
#!/bin/sh
HOST=$(echo $MEMCACHE | cut -d: -f1)
PORT=$(echo $MEMCACHE | cut -d: -f2)
echo "Waiting for memcached at $HOST:$PORT"
while ! nc -z -w 1 $HOST $PORT; do
sleep 1
echo "retrying..."
done
echo "Starting server"
/app/cryptgeon

View File

@@ -1,14 +1,13 @@
version: '3.8' version: '3.8'
services: services:
memcached: redis:
image: memcached:1-alpine image: redis:7-alpine
entrypoint: memcached -m 256 -I 128 # Limit to 128 MB Ram, customize at free will. -m must be at least double than -I.
app: app:
image: cupcakearmy/cryptgeon:latest image: cupcakearmy/cryptgeon:latest
depends_on: depends_on:
- memcached - redis
proxy: proxy:
image: nginx:alpine image: nginx:alpine

View File

@@ -7,7 +7,7 @@ This is a tiny guide to install cryptgeon on (probably) any unix system (and may
3. Run the cryptgeon. 3. Run the cryptgeon.
4. [Optional] install watchtower to keep up to date. 4. [Optional] install watchtower to keep up to date.
## Install Docker & DOcker Compose ## Install Docker & Docker Compose
- [Docker](https://docs.docker.com/engine/install/) - [Docker](https://docs.docker.com/engine/install/)
- [Compose](https://docs.docker.com/compose/install/) - [Compose](https://docs.docker.com/compose/install/)
@@ -107,16 +107,15 @@ networks:
external: true external: true
services: services:
memcached: redis:
image: memcached:1-alpine image: redis:7-alpine
restart: unless-stopped restart: unless-stopped
entrypoint: memcached -m 256M -I 4M # Limit to 128 MB Ram, customize at free will.
app: app:
image: cupcakearmy/cryptgeon:latest image: cupcakearmy/cryptgeon:latest
restart: unless-stopped restart: unless-stopped
depends_on: depends_on:
- memcached - redis
environment: environment:
SIZE_LIMIT: 4 MiB SIZE_LIMIT: 4 MiB
networks: networks:

View File

@@ -1,6 +1,7 @@
├─ MIT: 46 ├─ MIT: 12
├─ MIT*: 2 ├─ BSD-3-Clause: 1
├─ BSD-3-Clause: 2 ├─ (MPL-2.0 OR Apache-2.0): 1
├─ BSD-2-Clause: 1
├─ ISC: 1 ├─ ISC: 1
├─ 0BSD: 1 ├─ 0BSD: 1
└─ Apache-2.0: 1 └─ Apache-2.0: 1
1 ├─ MIT: 46 ├─ MIT: 12
2 ├─ MIT*: 2 ├─ BSD-3-Clause: 1
3 ├─ BSD-3-Clause: 2 ├─ (MPL-2.0 OR Apache-2.0): 1
4 ├─ BSD-2-Clause: 1
5 ├─ ISC: 1 ├─ ISC: 1
6 ├─ 0BSD: 1 ├─ 0BSD: 1
7 └─ Apache-2.0: 1 └─ Apache-2.0: 1

View File

@@ -4,13 +4,18 @@
"file": "Datei", "file": "Datei",
"advanced": "erweitert", "advanced": "erweitert",
"create": "erstellen", "create": "erstellen",
"loading": "Läd...", "loading": "läd",
"mode": "Modus", "mode": "Modus",
"views": "{n, plural, =0 {Ansichten} =1 {1 Ansicht} other {# Ansichten}}", "views": "{n, plural, =0 {Ansichten} =1 {1 Ansicht} other {# Ansichten}}",
"minutes": "{n, plural, =0 {Minuten} =1 {1 Minute} other {# Minuten}}", "minutes": "{n, plural, =0 {Minuten} =1 {1 Minute} other {# Minuten}}",
"max": "max", "max": "max",
"share_link": "Link teilen", "share_link": "Link teilen",
"copy_clipboard": "in die Zwischenablage kopieren" "copy_clipboard": "in die Zwischenablage kopieren",
"copied_to_clipboard": "in die Zwischenablage kopiert",
"encrypting": "verschlüsseln",
"decrypting": "entschlüsselt",
"uploading": "hochladen",
"downloading": "wird heruntergeladen"
}, },
"home": { "home": {
"intro": "Senden Sie ganz einfach <i>vollständig verschlüsselte</i>, sichere Notizen oder Dateien mit einem Klick. Erstellen Sie einfach eine Notiz und teilen Sie den Link.", "intro": "Senden Sie ganz einfach <i>vollständig verschlüsselte</i>, sichere Notizen oder Dateien mit einem Klick. Erstellen Sie einfach eine Notiz und teilen Sie den Link.",
@@ -23,12 +28,15 @@
"max": "max: {n}", "max": "max: {n}",
"empty_content": "Notiz ist leer." "empty_content": "Notiz ist leer."
}, },
"copied_to_clipboard": "in die Zwischenablage kopiert 🔗" "messages": {
"note_created": "notiz erstellt."
}
}, },
"show": { "show": {
"errors": { "errors": {
"not_found": "wurde nicht gefunden oder wurde bereits gelöscht.", "not_found": "wurde nicht gefunden oder wurde bereits gelöscht.",
"decryption_failed": "falsches Passwort. konnte nicht entziffert werden. wahrscheinlich ein defekter Link. Notiz wurde zerstört." "decryption_failed": "falsches Passwort. konnte nicht entziffert werden. wahrscheinlich ein defekter Link. Notiz wurde zerstört.",
"unsupported_type": "nicht unterstützter Notiztyp."
}, },
"explanation": "Klicken Sie unten, um die Notiz anzuzeigen und zu löschen, wenn der Zähler sein Limit erreicht hat", "explanation": "Klicken Sie unten, um die Notiz anzuzeigen und zu löschen, wenn der Zähler sein Limit erreicht hat",
"show_note": "Notiz anzeigen", "show_note": "Notiz anzeigen",

View File

@@ -4,13 +4,18 @@
"file": "file", "file": "file",
"advanced": "advanced", "advanced": "advanced",
"create": "create", "create": "create",
"loading": "loading...", "loading": "loading",
"mode": "mode", "mode": "mode",
"views": "{n, plural, =0 {views} =1 {1 view} other {# views}}", "views": "{n, plural, =0 {views} =1 {1 view} other {# views}}",
"minutes": "{n, plural, =0 {minutes} =1 {1 minute} other {# minutes}}", "minutes": "{n, plural, =0 {minutes} =1 {1 minute} other {# minutes}}",
"max": "max", "max": "max",
"share_link": "share link", "share_link": "share link",
"copy_clipboard": "copy to clipboard" "copy_clipboard": "copy to clipboard",
"copied_to_clipboard": "copied to clipboard",
"encrypting": "encrypting",
"decrypting": "decrypting",
"uploading": "uploading",
"downloading": "downloading"
}, },
"home": { "home": {
"intro": "Easily send <i>fully encrypted</i>, secure notes or files with one click. Just create a note and share the link.", "intro": "Easily send <i>fully encrypted</i>, secure notes or files with one click. Just create a note and share the link.",
@@ -23,12 +28,15 @@
"max": "max: {n}", "max": "max: {n}",
"empty_content": "note is empty." "empty_content": "note is empty."
}, },
"copied_to_clipboard": "copied to clipboard 🔗" "messages": {
"note_created": "note created."
}
}, },
"show": { "show": {
"errors": { "errors": {
"not_found": "note was not found or was already deleted.", "not_found": "note was not found or was already deleted.",
"decryption_failed": "wrong password. could not decipher. probably a broken link. note was destroyed." "decryption_failed": "wrong password. could not decipher. probably a broken link. note was destroyed.",
"unsupported_type": "unsupported note type."
}, },
"explanation": "click below to show and delete the note if the counter has reached it's limit", "explanation": "click below to show and delete the note if the counter has reached it's limit",
"show_note": "show note", "show_note": "show note",

View File

@@ -4,13 +4,18 @@
"file": "archivo", "file": "archivo",
"advanced": "avanzado", "advanced": "avanzado",
"create": "crear", "create": "crear",
"loading": "cargando...", "loading": "cargando",
"mode": "modo", "mode": "modo",
"views": "{n, plural, =0 {vistas} =1 {1 vista} other {# vistas}}", "views": "{n, plural, =0 {vistas} =1 {1 vista} other {# vistas}}",
"minutes": "{n, plural, =0 {minutos} =1 {1 minuto} other {# minutos}}", "minutes": "{n, plural, =0 {minutos} =1 {1 minuto} other {# minutos}}",
"max": "max", "max": "max",
"share_link": "compartir enlace", "share_link": "compartir enlace",
"copy_clipboard": "copiar al portapapeles" "copy_clipboard": "copiar al portapapeles",
"copied_to_clipboard": "copiado al portapapeles",
"encrypting": "encriptando",
"decrypting": "descifrando",
"uploading": "cargando",
"downloading": "descargando"
}, },
"home": { "home": {
"intro": "Envía fácilmente notas o archivos <i>totalmente encriptados</i> y seguros con un solo clic. Solo tienes que crear una nota y compartir el enlace.", "intro": "Envía fácilmente notas o archivos <i>totalmente encriptados</i> y seguros con un solo clic. Solo tienes que crear una nota y compartir el enlace.",
@@ -23,12 +28,15 @@
"max": "max: {n}", "max": "max: {n}",
"empty_content": "la nota está vacía." "empty_content": "la nota está vacía."
}, },
"copied_to_clipboard": "copiado al portapapeles 🔗" "messages": {
"note_created": "nota creada."
}
}, },
"show": { "show": {
"errors": { "errors": {
"not_found": "la nota no se encontró o ya fue borrada.", "not_found": "la nota no se encontró o ya fue borrada.",
"decryption_failed": "contraseña incorrecta. no se pudo descifrar. probablemente un enlace roto. la nota fue destruida." "decryption_failed": "contraseña incorrecta. no se pudo descifrar. probablemente un enlace roto. la nota fue destruida.",
"unsupported_type": "tipo de nota no compatible."
}, },
"explanation": "pulse abajo para mostrar y borrar la nota si el contador ha llegado a su límite", "explanation": "pulse abajo para mostrar y borrar la nota si el contador ha llegado a su límite",
"show_note": "mostrar nota", "show_note": "mostrar nota",

View File

@@ -4,13 +4,18 @@
"file": "fichier", "file": "fichier",
"advanced": "avancé", "advanced": "avancé",
"create": "créer", "create": "créer",
"loading": "chargement...", "loading": "chargement",
"mode": "mode", "mode": "mode",
"views": "{n, plural, =0 {vues} =1 {1 vue} other {# vues}}", "views": "{n, plural, =0 {vues} =1 {1 vue} other {# vues}}",
"minutes": "{n, plural, =0 {minutes} =1 {1 minute} other {# minutes}}", "minutes": "{n, plural, =0 {minutes} =1 {1 minute} other {# minutes}}",
"max": "max", "max": "max",
"share_link": "partager le lien", "share_link": "partager le lien",
"copy_clipboard": "copier dans le presse-papiers" "copy_clipboard": "copier dans le presse-papiers",
"copied_to_clipboard": "copié dans le presse-papiers",
"encrypting": "cryptage",
"decrypting": "déchiffrer",
"uploading": "téléchargement",
"downloading": "téléchargement"
}, },
"home": { "home": {
"intro": "Envoyez facilement des notes ou des fichiers <i>entièrement cryptés</i> et sécurisés en un seul clic. Il suffit de créer une note et de partager le lien.", "intro": "Envoyez facilement des notes ou des fichiers <i>entièrement cryptés</i> et sécurisés en un seul clic. Il suffit de créer une note et de partager le lien.",
@@ -23,12 +28,15 @@
"max": "max: {n}", "max": "max: {n}",
"empty_content": "La note est vide." "empty_content": "La note est vide."
}, },
"copied_to_clipboard": "copié dans le presse-papiers 🔗" "messages": {
"note_created": "note créée."
}
}, },
"show": { "show": {
"errors": { "errors": {
"not_found": "La note n'a pas été trouvée ou a déjà été supprimée.", "not_found": "La note n'a pas été trouvée ou a déjà été supprimée.",
"decryption_failed": "mauvais mot de passe. impossible à déchiffrer. probablement un lien brisé. la note a été détruite." "decryption_failed": "mauvais mot de passe. impossible à déchiffrer. probablement un lien brisé. la note a été détruite.",
"unsupported_type": "type de note non supporté."
}, },
"explanation": "Cliquez ci-dessous pour afficher et supprimer la note si le compteur a atteint sa limite.", "explanation": "Cliquez ci-dessous pour afficher et supprimer la note si le compteur a atteint sa limite.",
"show_note": "note de présentation", "show_note": "note de présentation",

View File

@@ -4,13 +4,18 @@
"file": "file", "file": "file",
"advanced": "avanzato", "advanced": "avanzato",
"create": "crea", "create": "crea",
"loading": "carica...", "loading": "carica",
"mode": "modalita", "mode": "modalita",
"views": "{n, plural, =0 {viste} =1 {1 vista} other {# viste}}", "views": "{n, plural, =0 {viste} =1 {1 vista} other {# viste}}",
"minutes": "{n, plural, =0 {minuti} =1 {1 minuto} other {# minuti}}", "minutes": "{n, plural, =0 {minuti} =1 {1 minuto} other {# minuti}}",
"max": "max", "max": "max",
"share_link": "condividi link", "share_link": "condividi link",
"copy_clipboard": "copia negli appunti" "copy_clipboard": "copia negli appunti",
"copied_to_clipboard": "copiato negli appunti",
"encrypting": "criptando",
"decrypting": "decifrando",
"uploading": "caricamento",
"downloading": "scaricando"
}, },
"home": { "home": {
"intro": "Invia facilmente note o file <i>completamente criptati</i> e sicuri con un solo clic. Basta creare una nota e condividere il link.", "intro": "Invia facilmente note o file <i>completamente criptati</i> e sicuri con un solo clic. Basta creare una nota e condividere il link.",
@@ -23,12 +28,15 @@
"max": "max: {n}", "max": "max: {n}",
"empty_content": "la nota è vuota." "empty_content": "la nota è vuota."
}, },
"copied_to_clipboard": "copiato negli appunti 🔗" "messages": {
"note_created": "nota creata."
}
}, },
"show": { "show": {
"errors": { "errors": {
"not_found": "non è stata trovata o è stata già cancellata.", "not_found": "non è stata trovata o è stata già cancellata.",
"decryption_failed": "password sbagliata. non ha potuto decifrare. probabilmente un link rotto. la nota è stata distrutta." "decryption_failed": "password sbagliata. non ha potuto decifrare. probabilmente un link rotto. la nota è stata distrutta.",
"unsupported_type": "tipo di nota non supportato."
}, },
"explanation": "clicca sotto per mostrare e cancellare la nota se il contatore ha raggiunto il suo limite", "explanation": "clicca sotto per mostrare e cancellare la nota se il contatore ha raggiunto il suo limite",
"show_note": "mostra la nota", "show_note": "mostra la nota",

View File

@@ -0,0 +1,50 @@
{
"common": {
"note": "密信",
"file": "上传文件",
"advanced": "高级设置",
"create": "创建",
"loading": "加载中",
"mode": "模式",
"views": "{n, plural, =0 {可查看次数} =1 {1 次查看} other {# 次查看}}",
"minutes": "{n, plural, =0 {有效期(分钟)} =1 {1 分钟} other {# 分钟}}",
"max": "最大值",
"share_link": "分享链接",
"copy_clipboard": "复制到剪切版",
"copied_to_clipboard": "已复制到剪切板",
"encrypting": "加密",
"decrypting": "解密",
"uploading": "上传",
"downloading": "下载"
},
"home": {
"intro": "一键轻松发送 <i>完全加密的</i> 密信或者文件。只需创建一个密信然后分享链接。",
"explanation": "该密信会在{type}后失效。",
"new_note": "新建密信",
"new_note_notice": "<b>可用性警示:</b><br />由于加密鸽的所有数据是全部保存在内存中的,所以如果加密鸽的可用内存被用光了那么它将会删除最早的密信以释放内存,因此不保证该密信的可用性。<br />(一般情况下是您应该是不会遇到这个问题,只是警示一下。)",
"errors": {
"note_to_big": "无法创建密信,这个密信太大了!",
"note_error": "无法创建密信,请再试一遍。",
"max": "最大文件大小: {n}",
"empty_content": "密信为空!"
},
"messages": {
"note_created": "注释创建。"
}
},
"show": {
"errors": {
"not_found": "该密信无法被找到或者它已经被删除了!",
"decryption_failed": "密钥错误!您可能不小心粘贴了一个不完整的链接或者正在尝试破解该密信!但无论如何,该密信已被销毁!",
"unsupported_type": "不支持的票据类型。"
},
"explanation": "点击下方的按钮可以查看密信,如果它到达了限制将会被删除",
"show_note": "查看密信",
"warning_will_not_see_again": "您将<b>无法</b>再次查看该密信",
"download_all": "下载全部"
},
"file_upload": {
"selected_files": "已选中的文件",
"no_files_selected": "没有文件被选中"
}
}

View File

@@ -1,35 +1,36 @@
{ {
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "svelte-kit dev", "dev": "vite dev",
"build": "svelte-kit build", "build": "vite build",
"preview": "svelte-kit preview", "preview": "vite preview --port 3000",
"check": "svelte-check --tsconfig tsconfig.json", "check": "svelte-check --tsconfig tsconfig.json",
"licenses": "license-checker --summary > licenses.csv", "licenses": "license-checker --summary > licenses.csv",
"locale:download": "node scripts/locale.js" "locale:download": "node scripts/locale.js"
}, },
"type": "module", "type": "module",
"devDependencies": { "devDependencies": {
"@lokalise/node-api": "^7.2.0", "@lokalise/node-api": "^7.3.1",
"@sveltejs/adapter-static": "^1.0.0-next.34", "@sveltejs/adapter-static": "^1.0.0-next.38",
"@sveltejs/kit": "^1.0.0-next.348", "@sveltejs/kit": "^1.0.0-next.384",
"@types/dompurify": "^2.3.3",
"@types/file-saver": "^2.0.5", "@types/file-saver": "^2.0.5",
"@types/sanitize-html": "^2.6.2", "@zerodevx/svelte-toast": "^0.7.2",
"adm-zip": "^0.5.9", "adm-zip": "^0.5.9",
"dotenv": "^16.0.1", "dotenv": "^16.0.1",
"svelte": "^3.48.0", "svelte": "^3.49.0",
"svelte-check": "^2.7.2", "svelte-check": "^2.8.0",
"svelte-intl-precompile": "^0.10.1", "svelte-intl-precompile": "^0.10.1",
"svelte-preprocess": "^4.10.7", "svelte-preprocess": "^4.10.7",
"tslib": "^2.4.0", "tslib": "^2.4.0",
"typescript": "^4.7.3", "typescript": "^4.7.4",
"vite": "^2.9.10" "vite": "^3.0.2"
}, },
"dependencies": { "dependencies": {
"@fontsource/fira-mono": "^4.5.8", "@fontsource/fira-mono": "^4.5.8",
"copy-to-clipboard": "^3.3.1", "copy-to-clipboard": "^3.3.1",
"dompurify": "^2.3.10",
"file-saver": "^2.0.5", "file-saver": "^2.0.5",
"pretty-bytes": "^5.6.0", "pretty-bytes": "^5.6.0"
"sanitize-html": "^2.7.0"
} }
} }

673
frontend/pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -14,7 +14,9 @@
--ui-text-0: #fefefe; --ui-text-0: #fefefe;
--ui-text-1: #eee; --ui-text-1: #eee;
--ui-clr-primary: hsl(186, 65%, 55%); --ui-clr-primary: hsl(186, 65%, 55%);
--ui-clr-primary-alt: hsl(186, 85%, 35%);
--ui-clr-error: hsl(357, 77%, 51%); --ui-clr-error: hsl(357, 77%, 51%);
--ui-clr-error-alt: hsl(357, 87%, 41%);
--ui-anim: all 150ms ease; --ui-anim: all 150ms ease;
} }

View File

@@ -0,0 +1,61 @@
import type { EncryptedFileDTO, FileDTO } from './api'
import { Crypto } from './crypto'
abstract class CryptAdapter<T> {
abstract encrypt(plaintext: T, key: CryptoKey): Promise<string>
abstract decrypt(ciphertext: string, key: CryptoKey): Promise<T>
}
class CryptTextAdapter implements CryptAdapter<string> {
async encrypt(plaintext: string, key: CryptoKey) {
return await Crypto.encrypt(new TextEncoder().encode(plaintext), key)
}
async decrypt(ciphertext: string, key: CryptoKey) {
const plaintext = await Crypto.decrypt(ciphertext, key)
return new TextDecoder().decode(plaintext)
}
}
class CryptBlobAdapter implements CryptAdapter<Blob> {
async encrypt(plaintext: Blob, key: CryptoKey) {
return await Crypto.encrypt(await plaintext.arrayBuffer(), key)
}
async decrypt(ciphertext: string, key: CryptoKey) {
const plaintext = await Crypto.decrypt(ciphertext, key)
return new Blob([plaintext], { type: 'application/octet-stream' })
}
}
class CryptFilesAdapter implements CryptAdapter<FileDTO[]> {
async encrypt(plaintext: FileDTO[], key: CryptoKey) {
const adapter = new CryptBlobAdapter()
const data: Promise<EncryptedFileDTO>[] = plaintext.map(async (file) => ({
name: file.name,
size: file.size,
type: file.type,
contents: await adapter.encrypt(file.contents, key),
}))
return JSON.stringify(await Promise.all(data))
}
async decrypt(ciphertext: string, key: CryptoKey) {
const adapter = new CryptBlobAdapter()
const data: EncryptedFileDTO[] = JSON.parse(ciphertext)
const files: FileDTO[] = await Promise.all(
data.map(async (file) => ({
name: file.name,
size: file.size,
type: file.type,
contents: await adapter.decrypt(file.contents, key),
}))
)
return files
}
}
export const Adapters = {
Text: new CryptTextAdapter(),
Blob: new CryptBlobAdapter(),
Files: new CryptFilesAdapter(),
}

View File

@@ -11,6 +11,10 @@ export type NotePublic = Pick<Note, 'contents' | 'meta'>
export type NoteCreate = Omit<Note, 'meta'> & { meta: string } export type NoteCreate = Omit<Note, 'meta'> & { meta: string }
export type FileDTO = Pick<File, 'name' | 'size' | 'type'> & { export type FileDTO = Pick<File, 'name' | 'size' | 'type'> & {
contents: Blob
}
export type EncryptedFileDTO = Omit<FileDTO, 'contents'> & {
contents: string contents: string
} }

View File

@@ -19,13 +19,31 @@ export class Hex {
} }
} }
const ALG = 'AES-GCM' export class ArrayBufferUtils {
static async toString(buffer: ArrayBuffer): Promise<string> {
const reader = new window.FileReader()
reader.readAsDataURL(new Blob([buffer]))
return new Promise((resolve) => {
reader.onloadend = () => resolve(reader.result as string)
})
}
export function getRandomBytes(size = 16): Uint8Array { static async fromString(s: string): Promise<ArrayBuffer> {
return window.crypto.getRandomValues(new Uint8Array(size)) return fetch(s)
.then((r) => r.blob())
.then((b) => b.arrayBuffer())
}
} }
export function getKeyFromString(password: string) { export class Crypto {
private static ALG = 'AES-GCM'
private static DELIMITER = ':::'
public static getRandomBytes(size: number): Uint8Array {
return window.crypto.getRandomValues(new Uint8Array(size))
}
public static getKeyFromString(password: string) {
return window.crypto.subtle.importKey( return window.crypto.subtle.importKey(
'raw', 'raw',
new TextEncoder().encode(password), new TextEncoder().encode(password),
@@ -33,9 +51,8 @@ export function getKeyFromString(password: string) {
false, false,
['deriveBits', 'deriveKey'] ['deriveBits', 'deriveKey']
) )
} }
public static async getDerivedForKey(key: CryptoKey, salt: ArrayBuffer) {
export async function getDerivedForKey(key: CryptoKey, salt: ArrayBuffer) {
const iterations = 100_000 const iterations = 100_000
return window.crypto.subtle.deriveKey( return window.crypto.subtle.deriveKey(
{ {
@@ -45,27 +62,36 @@ export async function getDerivedForKey(key: CryptoKey, salt: ArrayBuffer) {
hash: 'SHA-512', hash: 'SHA-512',
}, },
key, key,
{ name: ALG, length: 256 }, { name: this.ALG, length: 256 },
true, true,
['encrypt', 'decrypt'] ['encrypt', 'decrypt']
) )
} }
export async function encrypt(plaintext: string, key: CryptoKey) { public static async encrypt(plaintext: ArrayBuffer, key: CryptoKey): Promise<string> {
const salt = getRandomBytes(16) const salt = this.getRandomBytes(16)
const derived = await getDerivedForKey(key, salt) const derived = await this.getDerivedForKey(key, salt)
const iv = getRandomBytes(16) const iv = this.getRandomBytes(16)
const encrypted = await window.crypto.subtle.encrypt( const encrypted: ArrayBuffer = await window.crypto.subtle.encrypt(
{ name: ALG, iv }, { name: this.ALG, iv },
derived, derived,
new TextEncoder().encode(plaintext) plaintext
) )
return [salt, iv, encrypted].map(Hex.encode).join(':') const data = [
} Hex.encode(salt),
Hex.encode(iv),
await ArrayBufferUtils.toString(encrypted),
].join(this.DELIMITER)
return data
}
export async function decrypt(ciphertext: string, key: CryptoKey) { public static async decrypt(ciphertext: string, key: CryptoKey): Promise<ArrayBuffer> {
const [salt, iv, encrypted] = ciphertext.split(':').map(Hex.decode) const splitted = ciphertext.split(this.DELIMITER)
const derived = await getDerivedForKey(key, salt) const salt = Hex.decode(splitted[0])
const plaintext = await window.crypto.subtle.decrypt({ name: ALG, iv }, derived, encrypted) const iv = Hex.decode(splitted[1])
return new TextDecoder().decode(plaintext) const encrypted = await ArrayBufferUtils.fromString(splitted[2])
const derived = await this.getDerivedForKey(key, salt)
const plaintext = await window.crypto.subtle.decrypt({ name: this.ALG, iv }, derived, encrypted)
return plaintext
}
} }

View File

@@ -1,13 +0,0 @@
export class Files {
static toString(f: File | Blob): Promise<string> {
const reader = new window.FileReader()
reader.readAsDataURL(f)
return new Promise((resolve) => {
reader.onloadend = () => resolve(reader.result as string)
})
}
static async fromString(s: string): Promise<Blob> {
return fetch(s).then((r) => r.blob())
}
}

View File

@@ -7,6 +7,8 @@ export type Status = {
max_views: number max_views: number
max_expiration: number max_expiration: number
allow_advanced: boolean allow_advanced: boolean
theme_image: string
theme_text: string
} }
export const status = writable<null | Status>(null) export const status = writable<null | Status>(null)

37
frontend/src/lib/toast.ts Normal file
View File

@@ -0,0 +1,37 @@
import { toast, type SvelteToastOptions } from '@zerodevx/svelte-toast'
export enum NotifyType {
Success = 'success',
Error = 'error',
}
const themeMapping: Record<NotifyType, SvelteToastOptions['theme']> = {
[NotifyType.Success]: {
'--toastBackground': 'var(--ui-clr-primary)',
'--toastBarBackground': 'var(--ui-clr-primary-alt)',
},
[NotifyType.Error]: {
'--toastBackground': 'var(--ui-clr-error)',
'--toastBarBackground': 'var(--ui-clr-error-alt)',
},
}
function notifyFN(message: string, type: NotifyType = NotifyType.Success) {
const options: SvelteToastOptions = {
duration: 5_000,
theme: {
...themeMapping[type],
'--toastBarHeight': '0.25rem',
'--toastMinHeight': 'auto',
'--toastMsgPadding': '0.5rem',
'--toastBorderRadius': '0',
},
}
toast.push(message, options)
}
export const notify = {
success: (message: string) => notifyFN(message, NotifyType.Success),
error: (message: string) => notifyFN(message, NotifyType.Error),
}

View File

@@ -0,0 +1,54 @@
<script lang="ts">
import { t } from 'svelte-intl-precompile'
import type { Note } from '$lib/api'
import { status } from '$lib/stores/status'
import Switch from '$lib/ui/Switch.svelte'
import TextInput from '$lib/ui/TextInput.svelte'
export let note: Note
export let timeExpiration = false
</script>
<div class="fields">
<TextInput
data-testid="field-views"
type="number"
label={$t('common.views', { values: { n: 0 } })}
bind:value={note.views}
disabled={timeExpiration}
max={$status?.max_views}
validate={(v) =>
($status && v < $status?.max_views) ||
$t('home.errors.max', { values: { n: $status?.max_views ?? 0 } })}
/>
<div class="middle-switch">
<Switch
data-testid="switch-advanced-toggle"
label={$t('common.mode')}
bind:value={timeExpiration}
color={false}
/>
</div>
<TextInput
data-testid="field-expiration"
type="number"
label={$t('common.minutes', { values: { n: 0 } })}
bind:value={note.expiration}
disabled={!timeExpiration}
max={$status?.max_expiration}
validate={(v) =>
($status && v < $status?.max_expiration) ||
$t('home.errors.max', { values: { n: $status?.max_expiration ?? 0 } })}
/>
</div>
<style>
.middle-switch {
margin: 0 1rem;
}
.fields {
display: flex;
}
</style>

View File

@@ -1,38 +1,32 @@
<script lang="ts"> <script lang="ts">
import type { FileDTO } from '$lib/api'
import { Files } from '$lib/files'
import { createEventDispatcher } from 'svelte'
import { t } from 'svelte-intl-precompile' import { t } from 'svelte-intl-precompile'
import Button from './Button.svelte'
import MaxSize from './MaxSize.svelte' import type { FileDTO } from '$lib/api'
import Button from '$lib/ui/Button.svelte'
import MaxSize from '$lib/ui/MaxSize.svelte'
export let label: string = '' export let label: string = ''
let files: File[] = [] export let files: FileDTO[] = []
const dispatch = createEventDispatcher<{ file: string }>() function fileToDTO(file: File): FileDTO {
return {
name: file.name,
size: file.size,
type: file.type,
contents: file,
}
}
async function onInput(e: Event) { async function onInput(e: Event) {
const input = e.target as HTMLInputElement const input = e.target as HTMLInputElement
if (input?.files?.length) { if (input?.files?.length) {
files = [...files, ...Array.from(input.files)] files = [...files, ...Array.from(input.files).map(fileToDTO)]
const data: FileDTO[] = await Promise.all(
files.map(async (file) => ({
name: file.name,
type: file.type,
size: file.size,
contents: await Files.toString(file),
}))
)
dispatch('file', JSON.stringify(data))
} else {
dispatch('file', '')
} }
} }
function clear(e: Event) { function clear(e: Event) {
e.preventDefault() e.preventDefault()
files = [] files = []
dispatch('file', '')
} }
</script> </script>
@@ -40,7 +34,7 @@
<small> <small>
{label} {label}
</small> </small>
<input type="file" on:change={onInput} multiple /> <input {...$$restProps} type="file" on:change={onInput} multiple />
<div class="box"> <div class="box">
{#if files.length} {#if files.length}
<div> <div>
@@ -57,7 +51,9 @@
<div> <div>
<b>{$t('file_upload.no_files_selected')}</b> <b>{$t('file_upload.no_files_selected')}</b>
<br /> <br />
<small>{$t('common.max')}: <MaxSize /></small> <small>
{$t('common.max')}: <MaxSize />
</small>
</div> </div>
{/if} {/if}
</div> </div>

View File

@@ -0,0 +1,41 @@
<svg
version="1.1"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
viewBox="0 0 100 100"
xml:space="preserve"
>
<rect fill="none" stroke="currentColor" stroke-width="4" x="25" y="25" width="50" height="50">
<animateTransform
attributeName="transform"
dur="0.5s"
from="0 50 50"
to="180 50 50"
type="rotate"
id="strokeBox"
attributeType="XML"
begin="rectBox.end"
/>
</rect>
<rect x="27" y="27" fill="currentColor" width="46" height="50">
<animate
attributeName="height"
dur="1.3s"
attributeType="XML"
from="50"
to="0"
id="rectBox"
fill="freeze"
begin="0s;strokeBox.end"
/>
</rect>
</svg>
<style>
svg {
height: 2em;
position: relative;
top: 0.6em;
pointer-events: none;
}
</style>

After

Width:  |  Height:  |  Size: 784 B

View File

@@ -1,12 +1,17 @@
<script lang="ts"> <script lang="ts">
import { status } from '$lib/stores/status'
import prettyBytes from 'pretty-bytes' import prettyBytes from 'pretty-bytes'
import { _ } from 'svelte-intl-precompile' import { _ } from 'svelte-intl-precompile'
import { status } from '$lib/stores/status'
// Due to encoding overhead (~35%) with base64
// https://en.wikipedia.org/wiki/Base64
const overhead = 1 / 1.35
</script> </script>
<span> <span>
{#if $status !== null} {#if $status !== null}
{prettyBytes($status.max_size, { binary: true })} {prettyBytes($status.max_size * overhead, { binary: true })}
{:else} {:else}
{$_('common.loading')} {$_('common.loading')}
{/if} {/if}

View File

@@ -0,0 +1,37 @@
<script lang="ts" context="module">
export type NoteResult = {
password: string
id: string
}
</script>
<script lang="ts">
import { t } from 'svelte-intl-precompile'
import Button from '$lib/ui/Button.svelte'
import TextInput from '$lib/ui/TextInput.svelte'
export let result: NoteResult
function reset() {
window.location.reload()
}
</script>
<TextInput
type="text"
readonly
label={$t('common.share_link')}
value="{window.location.origin}/note/{result.id}#{result.password}"
copy
data-testid="share-link"
/>
<br />
<p>
{@html $t('home.new_note_notice')}
</p>
<br />
<Button on:click={reset}>{$t('home.new_note')}</Button>
<style>
</style>

View File

@@ -1,20 +1,24 @@
<script lang="ts" context="module">
export type DecryptedNote = Omit<NotePublic, 'contents'> & { contents: any }
</script>
<script lang="ts"> <script lang="ts">
import type { FileDTO, NotePublic } from '$lib/api' import DOMPurify from 'dompurify'
import { Files } from '$lib/files'
import copy from 'copy-to-clipboard'
import { saveAs } from 'file-saver' import { saveAs } from 'file-saver'
import prettyBytes from 'pretty-bytes' import prettyBytes from 'pretty-bytes'
import sanitize from 'sanitize-html'
import { t } from 'svelte-intl-precompile' import { t } from 'svelte-intl-precompile'
import Button from './Button.svelte'
export let note: NotePublic import type { FileDTO, NotePublic } from '$lib/api'
import Button from '$lib/ui/Button.svelte'
import { copy } from '$lib/utils'
export let note: DecryptedNote
const RE_URL = /[A-Za-z]+:\/\/([A-Z a-z0-9\-._~:\/?#\[\]@!$&'()*+,;%=])+/g const RE_URL = /[A-Za-z]+:\/\/([A-Z a-z0-9\-._~:\/?#\[\]@!$&'()*+,;%=])+/g
let files: FileDTO[] = [] let files: FileDTO[] = []
$: if (note.meta.type === 'file') { $: if (note.meta.type === 'file') {
files = JSON.parse(note.contents) as FileDTO[] files = note.contents
} }
$: download = () => { $: download = () => {
@@ -24,28 +28,29 @@
} }
async function downloadFile(file: FileDTO) { async function downloadFile(file: FileDTO) {
const f = new File([await Files.fromString(file.contents)], file.name, { const f = new File([file.contents], file.name, {
type: file.type, type: file.type,
}) })
saveAs(f) saveAs(f)
} }
function contentWithLinks(content: string): string { function contentWithLinks(content: string): string {
const replaced = note.contents.replace( const replaced = content.replace(
RE_URL, RE_URL,
(url) => `<a href="${url}" rel="noreferrer">${url}</a>` (url) => `<a href="${url}" rel="noreferrer">${url}</a>`
) )
return sanitize(replaced, { allowedTags: ['a'], allowedAttributes: { a: ['href', 'rel'] } }) return DOMPurify.sanitize(replaced, { USE_PROFILES: { html: true } })
} }
</script> </script>
<p class="error-text">{@html $t('show.warning_will_not_see_again')}</p> <p class="error-text">{@html $t('show.warning_will_not_see_again')}</p>
{#if note.meta.type === 'text'} <div data-testid="result">
{#if note.meta.type === 'text'}
<div class="note"> <div class="note">
{@html contentWithLinks(note.contents)} {@html contentWithLinks(note.contents)}
</div> </div>
<Button on:click={() => copy(note.contents)}>{$t('common.copy_clipboard')}</Button> <Button on:click={() => copy(note.contents)}>{$t('common.copy_clipboard')}</Button>
{:else} {:else}
{#each files as file} {#each files as file}
<div class="note file"> <div class="note file">
<b on:click={() => downloadFile(file)}> {file.name}</b> <b on:click={() => downloadFile(file)}> {file.name}</b>
@@ -53,7 +58,8 @@
</div> </div>
{/each} {/each}
<Button on:click={download}>{$t('show.download_all')}</Button> <Button on:click={download}>{$t('show.download_all')}</Button>
{/if} {/if}
</div>
<style> <style>
.note { .note {

View File

@@ -1,9 +1,7 @@
<script lang="ts"> <script lang="ts">
import { getRandomBytes, Hex } from '$lib/crypto' import { Crypto, Hex } from '$lib/crypto'
import copyToClipboard from 'copy-to-clipboard' import Icon from '$lib/ui/Icon.svelte'
import { t } from 'svelte-intl-precompile' import { copy as copyFN } from '$lib/utils'
import { fade } from 'svelte/transition'
import Icon from './Icon.svelte'
export let label: string = '' export let label: string = ''
export let value: any export let value: any
@@ -14,8 +12,6 @@
const initialType = $$restProps.type const initialType = $$restProps.type
const isPassword = initialType === 'password' const isPassword = initialType === 'password'
let hidden = true let hidden = true
let notification: string | null = null
let notificationTimeout: NodeJS.Timeout | null = null
$: valid = validate(value) $: valid = validate(value)
@@ -27,22 +23,8 @@
function toggle() { function toggle() {
hidden = !hidden hidden = !hidden
} }
function copyFN() {
copyToClipboard(value.toString())
notify($t('home.copied_to_clipboard'))
}
function randomFN() { function randomFN() {
value = Hex.encode(getRandomBytes(20)) value = Hex.encode(Crypto.getRandomBytes(20))
}
function notify(msg: string, delay: number = 2000) {
if (notificationTimeout) {
clearTimeout(notificationTimeout)
}
notificationTimeout = setTimeout(() => {
notification = null
}, delay)
notification = msg
} }
</script> </script>
@@ -62,12 +44,9 @@
<Icon class="icon" icon="dice" on:click={randomFN} /> <Icon class="icon" icon="dice" on:click={randomFN} />
{/if} {/if}
{#if copy} {#if copy}
<Icon class="icon" icon="copy" on:click={copyFN} /> <Icon class="icon" icon="copy" on:click={() => copyFN(value.toString())} />
{/if} {/if}
</div> </div>
{#if notification}
<div class="notification" transition:fade><small>{notification}</small></div>
{/if}
</label> </label>
<style> <style>
@@ -117,11 +96,4 @@
.icons > :global(.icon:hover) { .icons > :global(.icon:hover) {
border-color: var(--ui-clr-primary); border-color: var(--ui-clr-primary);
} }
.notification {
text-align: right;
position: absolute;
right: 0;
bottom: -1.5em;
}
</style> </style>

11
frontend/src/lib/utils.ts Normal file
View File

@@ -0,0 +1,11 @@
import copyToClipboard from 'copy-to-clipboard'
import { t } from 'svelte-intl-precompile'
import { get } from 'svelte/store'
import { notify } from './toast'
export function copy(value: string) {
copyToClipboard(value)
const msg = get(t)('common.copied_to_clipboard')
notify.success(msg)
}

View File

@@ -1,29 +1,35 @@
<script lang="ts"> <script lang="ts">
import { create, Note, PayloadToLargeError } from '$lib/api'
import { encrypt, getKeyFromString, getRandomBytes, Hex } from '$lib/crypto'
import { status } from '$lib/stores/status'
import Button from '$lib/ui/Button.svelte'
import FileUpload from '$lib/ui/FileUpload.svelte'
import MaxSize from '$lib/ui/MaxSize.svelte'
import Switch from '$lib/ui/Switch.svelte'
import TextArea from '$lib/ui/TextArea.svelte'
import TextInput from '$lib/ui/TextInput.svelte'
import { t } from 'svelte-intl-precompile' import { t } from 'svelte-intl-precompile'
import { blur } from 'svelte/transition' import { blur } from 'svelte/transition'
import { Adapters } from '$lib/adapters'
import type { FileDTO, Note } from '$lib/api'
import { create, PayloadToLargeError } from '$lib/api'
import { Crypto, Hex } from '$lib/crypto'
import { status } from '$lib/stores/status'
import { notify } from '$lib/toast'
import AdvancedParameters from '$lib/ui/AdvancedParameters.svelte'
import Button from '$lib/ui/Button.svelte'
import FileUpload from '$lib/ui/FileUpload.svelte'
import Loader from '$lib/ui/Loader.svelte'
import MaxSize from '$lib/ui/MaxSize.svelte'
import Result, { type NoteResult } from '$lib/ui/NoteResult.svelte'
import Switch from '$lib/ui/Switch.svelte'
import TextArea from '$lib/ui/TextArea.svelte'
let note: Note = { let note: Note = {
contents: '', contents: '',
meta: { type: 'text' }, meta: { type: 'text' },
views: 1, views: 1,
expiration: 60, expiration: 60,
} }
let result: { password: string; id: string } | null = null let files: FileDTO[]
let result: NoteResult | null = null
let advanced = false let advanced = false
let file = false let isFile = false
let timeExpiration = false let timeExpiration = false
let message = '' let description = ''
let loading = false let loading: string | null = null
let error: string | null = null
$: if (!advanced) { $: if (!advanced) {
note.views = 1 note.views = 1
@@ -31,7 +37,7 @@
} }
$: { $: {
message = $t('home.explanation', { description = $t('home.explanation', {
values: { values: {
type: $t(timeExpiration ? 'common.minutes' : 'common.views', { type: $t(timeExpiration ? 'common.minutes' : 'common.views', {
values: { n: (timeExpiration ? note.expiration : note.views) ?? '?' }, values: { n: (timeExpiration ? note.expiration : note.views) ?? '?' },
@@ -40,9 +46,9 @@
}) })
} }
$: note.meta.type = file ? 'file' : 'text' $: note.meta.type = isFile ? 'file' : 'text'
$: if (!file) { $: if (!isFile) {
note.contents = '' note.contents = ''
} }
@@ -50,71 +56,79 @@
async function submit() { async function submit() {
try { try {
error = null loading = $t('common.encrypting')
loading = true
const password = Hex.encode(getRandomBytes(32)) const password = Hex.encode(Crypto.getRandomBytes(32))
const key = await getKeyFromString(password) const key = await Crypto.getKeyFromString(password)
if (note.contents === '') throw new EmptyContentError()
const data: Note = { const data: Note = {
contents: await encrypt(note.contents, key), contents: '',
meta: note.meta, meta: note.meta,
} }
if (isFile) {
if (files.length === 0) throw new EmptyContentError()
data.contents = await Adapters.Files.encrypt(files, key)
} else {
if (note.contents === '') throw new EmptyContentError()
data.contents = await Adapters.Text.encrypt(note.contents, key)
}
if (timeExpiration) data.expiration = parseInt(note.expiration as any) if (timeExpiration) data.expiration = parseInt(note.expiration as any)
else data.views = parseInt(note.views as any) else data.views = parseInt(note.views as any)
loading = $t('common.uploading')
const response = await create(data) const response = await create(data)
result = { result = {
password: password, password: password,
id: response.id, id: response.id,
} }
notify.success($t('home.messages.note_created'))
} catch (e) { } catch (e) {
if (e instanceof PayloadToLargeError) { if (e instanceof PayloadToLargeError) {
error = $t('home.errors.note_to_big') notify.error($t('home.errors.note_to_big'))
} else if (e instanceof EmptyContentError) { } else if (e instanceof EmptyContentError) {
error = $t('home.errors.empty_content') notify.error($t('home.errors.empty_content'))
} else { } else {
error = $t('home.errors.note_error') console.error(e)
notify.error($t('home.errors.note_error'))
} }
} finally { } finally {
loading = false loading = null
} }
} }
function reset() {
window.location.reload()
}
</script> </script>
{#if result} {#if result}
<TextInput <Result {result} />
type="text"
readonly
label={$t('common.share_link')}
value="{window.location.origin}/note/{result.id}#{result.password}"
copy
/>
<br />
<p>
{@html $t('home.new_note_notice')}
</p>
<br />
<Button on:click={reset}>{$t('home.new_note')}</Button>
{:else} {:else}
<p> <p>
{@html $t('home.intro')} {@html $status?.theme_text || $t('home.intro')}
</p> </p>
<form on:submit|preventDefault={submit}> <form on:submit|preventDefault={submit}>
<fieldset disabled={loading}> <fieldset disabled={loading !== null}>
{#if file} {#if isFile}
<FileUpload label={$t('common.file')} on:file={(f) => (note.contents = f.detail)} /> <FileUpload data-testid="file-upload" label={$t('common.file')} bind:files />
{:else} {:else}
<TextArea label={$t('common.note')} bind:value={note.contents} placeholder="..." /> <TextArea
data-testid="text-field"
label={$t('common.note')}
bind:value={note.contents}
placeholder="..."
/>
{/if} {/if}
<div class="bottom"> <div class="bottom">
<Switch class="file" label={$t('common.file')} bind:value={file} /> <Switch
data-testid="switch-file"
class="file"
label={$t('common.file')}
bind:value={isFile}
/>
{#if $status?.allow_advanced} {#if $status?.allow_advanced}
<Switch label={$t('common.advanced')} bind:value={advanced} /> <Switch
data-testid="switch-advanced"
label={$t('common.advanced')}
bind:value={advanced}
/>
{/if} {/if}
<div class="grow" /> <div class="grow" />
<div class="tr"> <div class="tr">
@@ -124,47 +138,19 @@
</div> </div>
</div> </div>
{#if error}
<div class="error-text">{error}</div>
{/if}
<p> <p>
<br /> <br />
{#if loading} {#if loading}
{$t('common.loading')} {loading} <Loader />
{:else} {:else}
{message} {description}
{/if} {/if}
</p> </p>
{#if advanced} {#if advanced}
<div transition:blur={{ duration: 250 }}> <div transition:blur={{ duration: 250 }}>
<br /> <br />
<div class="fields"> <AdvancedParameters bind:note bind:timeExpiration />
<TextInput
type="number"
label={$t('common.views', { values: { n: 0 } })}
bind:value={note.views}
disabled={timeExpiration}
max={$status?.max_views}
validate={(v) =>
($status && v < $status?.max_views) ||
$t('home.errors.max', { values: { n: $status?.max_views ?? 0 } })}
/>
<div class="middle-switch">
<Switch label={$t('common.mode')} bind:value={timeExpiration} color={false} />
</div>
<TextInput
type="number"
label={$t('common.minutes', { values: { n: 0 } })}
bind:value={note.expiration}
disabled={!timeExpiration}
max={$status?.max_expiration}
validate={(v) =>
($status && v < $status?.max_expiration) ||
$t('home.errors.max', { values: { n: $status?.max_expiration ?? 0 } })}
/>
</div>
</div> </div>
{/if} {/if}
</fieldset> </fieldset>
@@ -185,16 +171,4 @@
.grow { .grow {
flex: 1; flex: 1;
} }
.middle-switch {
margin: 0 1rem;
}
.error-text {
margin-top: 0.5rem;
}
.fields {
display: flex;
}
</style> </style>

View File

@@ -1,5 +1,12 @@
<script lang="ts">
import { status } from '$lib/stores/status'
</script>
<header> <header>
<a href="/"> <a href="/">
{#if $status?.theme_image}
<img alt="logo" src={$status.theme_image} />
{:else}
<svg <svg
width="100%" width="100%"
height="100%" height="100%"
@@ -10,7 +17,8 @@
xml:space="preserve" xml:space="preserve"
style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;"
><g id="Logo" ><g id="Logo"
><clipPath id="_clip1"><rect x="6.336" y="3.225" width="193.55" height="193.55" /></clipPath ><clipPath id="_clip1"
><rect x="6.336" y="3.225" width="193.55" height="193.55" /></clipPath
><g clip-path="url(#_clip1)" ><g clip-path="url(#_clip1)"
><g ><g
><g ><g
@@ -73,6 +81,7 @@
></g ></g
></svg ></svg
> >
{/if}
</a> </a>
</header> </header>
@@ -87,20 +96,26 @@
margin-bottom: 2rem; margin-bottom: 2rem;
} }
img {
object-fit: contain;
}
@media screen and (max-width: 30rem) { @media screen and (max-width: 30rem) {
header { header {
margin-top: 1rem; margin-top: 1rem;
margin-bottom: 1rem; margin-bottom: 1rem;
} }
header svg { header svg,
header img {
max-height: 4rem; max-height: 4rem;
} }
} }
header svg { header svg,
header img {
width: 100%; width: 100%;
max-width: 16rem; max-height: 8rem;
transform: translateX(-1rem); transform: translateX(-1rem);
fill: currentColor; fill: currentColor;
} }

View File

@@ -1,5 +1,5 @@
<script lang="ts" context="module"> <script lang="ts" context="module">
import { init, waitLocale, getLocaleFromNavigator } from 'svelte-intl-precompile' import { getLocaleFromNavigator, init, waitLocale } from 'svelte-intl-precompile'
// @ts-ignore // @ts-ignore
import { registerAll } from '$locales' import { registerAll } from '$locales'
registerAll() registerAll()
@@ -7,11 +7,14 @@
</script> </script>
<script lang="ts"> <script lang="ts">
import { SvelteToast } from '@zerodevx/svelte-toast'
import { onMount } from 'svelte'
import '../app.css'
import { init as initStores } from '$lib/stores/status' import { init as initStores } from '$lib/stores/status'
import Footer from '$lib/views/Footer.svelte' import Footer from '$lib/views/Footer.svelte'
import Header from '$lib/views/Header.svelte' import Header from '$lib/views/Header.svelte'
import { onMount } from 'svelte'
import '../app.css'
onMount(() => { onMount(() => {
initStores() initStores()
@@ -28,6 +31,8 @@
<slot /> <slot />
</main> </main>
<SvelteToast />
<Footer /> <Footer />
{/await} {/await}

View File

@@ -12,47 +12,67 @@
import { onMount } from 'svelte' import { onMount } from 'svelte'
import { t } from 'svelte-intl-precompile' import { t } from 'svelte-intl-precompile'
import type { NotePublic } from '$lib/api' import { Adapters } from '$lib/adapters'
import { get, info } from '$lib/api' import { get, info } from '$lib/api'
import { decrypt, getKeyFromString } from '$lib/crypto' import { Crypto } from '$lib/crypto'
import Button from '$lib/ui/Button.svelte' import Button from '$lib/ui/Button.svelte'
import ShowNote from '$lib/ui/ShowNote.svelte' import Loader from '$lib/ui/Loader.svelte'
import ShowNote, { type DecryptedNote } from '$lib/ui/ShowNote.svelte'
export let id: string export let id: string
let password: string let password: string
let note: NotePublic | null = null let note: DecryptedNote | null = null
let exists = false let exists = false
let loading = true let loading: string | null = null
let error = false let error: string | null = null
onMount(async () => { onMount(async () => {
// Check if note exists
try { try {
loading = true loading = $t('common.loading')
error = false
password = window.location.hash.slice(1) password = window.location.hash.slice(1)
await info(id) await info(id)
exists = true exists = true
} catch { } catch {
exists = false exists = false
} finally { } finally {
loading = false loading = null
} }
}) })
/**
* Get the actual contents of the note and decrypt it.
*/
async function show() { async function show() {
try { try {
error = false error = null
loading = true loading = $t('common.downloading')
const data = note || (await get(id)) // Don't get the content twice on wrong password. const data = await get(id)
const key = await getKeyFromString(password) loading = $t('common.decrypting')
data.contents = await decrypt(data.contents, key) const key = await Crypto.getKeyFromString(password)
note = data switch (data.meta.type) {
case 'text':
note = {
meta: { type: 'text' },
contents: await Adapters.Text.decrypt(data.contents, key),
}
break
case 'file':
note = {
meta: { type: 'file' },
contents: await Adapters.Files.decrypt(data.contents, key),
}
break
default:
error = $t('show.errors.unsupported_type')
return
}
} catch { } catch {
error = true error = $t('show.errors.decryption_failed')
} finally { } finally {
loading = false loading = null
} }
} }
</script> </script>
@@ -66,11 +86,11 @@
<form on:submit|preventDefault={show}> <form on:submit|preventDefault={show}>
<fieldset> <fieldset>
<p>{$t('show.explanation')}</p> <p>{$t('show.explanation')}</p>
<Button type="submit">{$t('show.show_note')}</Button> <Button data-testid="show-note-button" type="submit">{$t('show.show_note')}</Button>
{#if error} {#if error}
<br /> <br />
<p class="error-text"> <p class="error-text">
{$t('show.errors.decryption_failed')} {error}
<br /> <br />
</p> </p>
{/if} {/if}
@@ -79,5 +99,11 @@
{/if} {/if}
{/if} {/if}
{#if loading} {#if loading}
<p>{$t('common.loading')}</p> <p class="loader">{loading} <Loader /></p>
{/if} {/if}
<style>
.loader {
text-align: center;
}
</style>

View File

@@ -1,6 +1,5 @@
import preprocess from 'svelte-preprocess'
import adapter from '@sveltejs/adapter-static' import adapter from '@sveltejs/adapter-static'
import precompileIntl from 'svelte-intl-precompile/sveltekit-plugin' import preprocess from 'svelte-preprocess'
export default { export default {
preprocess: preprocess(), preprocess: preprocess(),
@@ -9,10 +8,5 @@ export default {
adapter: adapter({ adapter: adapter({
fallback: 'index.html', fallback: 'index.html',
}), }),
vite: {
plugins: [
precompileIntl('locales'), // if your translations are defined in /locales/[lang].json
],
},
}, },
} }

View File

@@ -1,3 +1,7 @@
{ {
"extends": "./.svelte-kit/tsconfig.json" "extends": "./.svelte-kit/tsconfig.json",
"compilerOptions": {
"strict": true,
"allowSyntheticDefaultImports": true
}
} }

12
frontend/vite.config.js Normal file
View File

@@ -0,0 +1,12 @@
import { sveltekit } from '@sveltejs/kit/vite'
import precompileIntl from 'svelte-intl-precompile/sveltekit-plugin'
/** @type {import('vite').UserConfig} */
const config = {
server: {
port: 3000,
},
plugins: [sveltekit(), precompileIntl('locales')],
}
export default config

View File

@@ -1,12 +1,23 @@
{ {
"scripts": { "scripts": {
"dev:docker": "docker-compose up memcached", "dev:docker": "docker-compose up redis",
"dev:backend": "cd backend && cargo watch -x 'run --bin cryptgeon'", "dev:backend": "cd backend && cargo watch -x 'run --bin cryptgeon'",
"dev:front": "pnpm --prefix frontend run dev", "dev:front": "pnpm --prefix frontend run dev",
"dev:proxy": "node proxy.mjs", "dev:proxy": "node proxy.mjs",
"dev": "run-p dev:*" "dev": "run-p dev:*",
"test": "playwright test --project chrome firefox safari",
"test:local": "playwright test --project local",
"ci:server": "cd backend && SIZE_LIMIT=10MiB LISTEN_ADDR=0.0.0.0:1234 cargo run",
"ci:server:backend": "cd backend && cargo run",
"ci:server:front": "pnpm --prefix frontend run preview",
"ci:server:proxy": "node proxy.mjs",
"ci:prepare": "run-p ci:prepare:*",
"ci:prepare:backend": "cd backend && cargo build",
"ci:prepare:front": "pnpm --prefix frontend install && pnpm --prefix frontend run build"
}, },
"devDependencies": { "devDependencies": {
"@playwright/test": "^1.23.4",
"@types/node": "16",
"http-proxy": "^1.18.1", "http-proxy": "^1.18.1",
"npm-run-all": "^4.1.5" "npm-run-all": "^4.1.5"
} }

30
playwright.config.ts Normal file
View File

@@ -0,0 +1,30 @@
import { devices, type PlaywrightTestConfig } from '@playwright/test'
const config: PlaywrightTestConfig = {
use: {
video: 'retain-on-failure',
baseURL: 'http://localhost:1234',
actionTimeout: 60_000,
},
outputDir: './test-results',
testDir: './test',
webServer: {
command: 'pnpm run ci:server',
port: 1234,
reuseExistingServer: true,
},
projects: [
{ name: 'chrome', use: { ...devices['Desktop Chrome'] } },
{ name: 'firefox', use: { ...devices['Desktop Firefox'] } },
{ name: 'safari', use: { ...devices['Desktop Safari'] } },
{
name: 'local',
use: { ...devices['Desktop Chrome'] },
// testMatch: 'file/too-big.spec.ts',
},
],
}
export default config

55
pnpm-lock.yaml generated
View File

@@ -1,15 +1,36 @@
lockfileVersion: 5.4 lockfileVersion: 5.4
specifiers: specifiers:
'@playwright/test': ^1.23.4
'@types/node': '16'
http-proxy: ^1.18.1 http-proxy: ^1.18.1
npm-run-all: ^4.1.5 npm-run-all: ^4.1.5
devDependencies: devDependencies:
'@playwright/test': 1.23.4
'@types/node': 16.11.45
http-proxy: 1.18.1 http-proxy: 1.18.1
npm-run-all: 4.1.5 npm-run-all: 4.1.5
packages: packages:
/@playwright/test/1.23.4:
resolution: {integrity: sha512-iIsoMJDS/lyuhw82FtcV/B3PXikgVD3hNe5hyvOpRM0uRr1OIpN3LgPeRbBjhzBWmyf6RgRg5fqK5sVcpA03yA==}
engines: {node: '>=14'}
hasBin: true
dependencies:
'@types/node': 18.0.6
playwright-core: 1.23.4
dev: true
/@types/node/16.11.45:
resolution: {integrity: sha512-3rKg/L5x0rofKuuUt5zlXzOnKyIHXmIu5R8A0TuNDMF2062/AOIDBciFIjToLEJ/9F9DzkHNot+BpNsMI1OLdQ==}
dev: true
/@types/node/18.0.6:
resolution: {integrity: sha512-/xUq6H2aQm261exT6iZTMifUySEt4GR5KX8eYyY+C4MSNPqSh9oNIP7tz2GLKTlFaiBbgZNxffoR3CVRG+cljw==}
dev: true
/ansi-styles/3.2.1: /ansi-styles/3.2.1:
resolution: {integrity: sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==} resolution: {integrity: sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==}
engines: {node: '>=4'} engines: {node: '>=4'}
@@ -32,7 +53,7 @@ packages:
resolution: {integrity: sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==} resolution: {integrity: sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==}
dependencies: dependencies:
function-bind: 1.1.1 function-bind: 1.1.1
get-intrinsic: 1.1.1 get-intrinsic: 1.1.2
dev: true dev: true
/chalk/2.4.2: /chalk/2.4.2:
@@ -91,7 +112,7 @@ packages:
es-to-primitive: 1.2.1 es-to-primitive: 1.2.1
function-bind: 1.1.1 function-bind: 1.1.1
function.prototype.name: 1.1.5 function.prototype.name: 1.1.5
get-intrinsic: 1.1.1 get-intrinsic: 1.1.2
get-symbol-description: 1.0.0 get-symbol-description: 1.0.0
has: 1.0.3 has: 1.0.3
has-property-descriptors: 1.0.0 has-property-descriptors: 1.0.0
@@ -158,8 +179,8 @@ packages:
resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==} resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==}
dev: true dev: true
/get-intrinsic/1.1.1: /get-intrinsic/1.1.2:
resolution: {integrity: sha512-kWZrnVM42QCiEA2Ig1bG8zjoIMOgxWwYCEeNdwY6Tv/cOSeGpcoX4pXHfKUxNKVoArnrEr2e9srnAxxGIraS9Q==} resolution: {integrity: sha512-Jfm3OyCxHh9DJyc28qGk+JmfkpO41A4XkneDSujN9MDXrm4oDKdHvndhZ2dN94+ERNfkYJWDclW6k2L/ZGHjXA==}
dependencies: dependencies:
function-bind: 1.1.1 function-bind: 1.1.1
has: 1.0.3 has: 1.0.3
@@ -171,7 +192,7 @@ packages:
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
dependencies: dependencies:
call-bind: 1.0.2 call-bind: 1.0.2
get-intrinsic: 1.1.1 get-intrinsic: 1.1.2
dev: true dev: true
/graceful-fs/4.2.10: /graceful-fs/4.2.10:
@@ -190,7 +211,7 @@ packages:
/has-property-descriptors/1.0.0: /has-property-descriptors/1.0.0:
resolution: {integrity: sha512-62DVLZGoiEBDHQyqG4w9xCuZ7eJEwNmJRWw2VY84Oedb7WFcA27fiEVe8oUQx9hAUJ4ekurquucTGwsyO1XGdQ==} resolution: {integrity: sha512-62DVLZGoiEBDHQyqG4w9xCuZ7eJEwNmJRWw2VY84Oedb7WFcA27fiEVe8oUQx9hAUJ4ekurquucTGwsyO1XGdQ==}
dependencies: dependencies:
get-intrinsic: 1.1.1 get-intrinsic: 1.1.2
dev: true dev: true
/has-symbols/1.0.3: /has-symbols/1.0.3:
@@ -231,7 +252,7 @@ packages:
resolution: {integrity: sha512-O0DB1JC/sPyZl7cIo78n5dR7eUSwwpYPiXRhTzNxZVAMUuB8vlnRFyLxdrVToks6XPLVnFfbzaVd5WLjhgg+vA==} resolution: {integrity: sha512-O0DB1JC/sPyZl7cIo78n5dR7eUSwwpYPiXRhTzNxZVAMUuB8vlnRFyLxdrVToks6XPLVnFfbzaVd5WLjhgg+vA==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
dependencies: dependencies:
get-intrinsic: 1.1.1 get-intrinsic: 1.1.2
has: 1.0.3 has: 1.0.3
side-channel: 1.0.4 side-channel: 1.0.4
dev: true dev: true
@@ -355,7 +376,7 @@ packages:
resolution: {integrity: sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==} resolution: {integrity: sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==}
dependencies: dependencies:
hosted-git-info: 2.8.9 hosted-git-info: 2.8.9
resolve: 1.22.0 resolve: 1.22.1
semver: 5.7.1 semver: 5.7.1
validate-npm-package-license: 3.0.4 validate-npm-package-license: 3.0.4
dev: true dev: true
@@ -430,6 +451,12 @@ packages:
engines: {node: '>=4'} engines: {node: '>=4'}
dev: true dev: true
/playwright-core/1.23.4:
resolution: {integrity: sha512-h5V2yw7d8xIwotjyNrkLF13nV9RiiZLHdXeHo+nVJIYGVlZ8U2qV0pMxNJKNTvfQVT0N8/A4CW6/4EW2cOcTiA==}
engines: {node: '>=14'}
hasBin: true
dev: true
/read-pkg/3.0.0: /read-pkg/3.0.0:
resolution: {integrity: sha512-BLq/cCO9two+lBgiTYNqD6GdtK8s4NpaWrl6/rCO9w0TUS8oJl7cmToOZfRYllKTISY6nt1U7jQ53brmKqY6BA==} resolution: {integrity: sha512-BLq/cCO9two+lBgiTYNqD6GdtK8s4NpaWrl6/rCO9w0TUS8oJl7cmToOZfRYllKTISY6nt1U7jQ53brmKqY6BA==}
engines: {node: '>=4'} engines: {node: '>=4'}
@@ -452,8 +479,8 @@ packages:
resolution: {integrity: sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==} resolution: {integrity: sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==}
dev: true dev: true
/resolve/1.22.0: /resolve/1.22.1:
resolution: {integrity: sha512-Hhtrw0nLeSrFQ7phPp4OOcVjLPIeMnRlr5mcnVuMe7M/7eBn98A3hmFRLoFo3DLZkivSYwhRUJTyPyWAk56WLw==} resolution: {integrity: sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw==}
hasBin: true hasBin: true
dependencies: dependencies:
is-core-module: 2.9.0 is-core-module: 2.9.0
@@ -467,14 +494,14 @@ packages:
dev: true dev: true
/shebang-command/1.2.0: /shebang-command/1.2.0:
resolution: {integrity: sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=} resolution: {integrity: sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
dependencies: dependencies:
shebang-regex: 1.0.0 shebang-regex: 1.0.0
dev: true dev: true
/shebang-regex/1.0.0: /shebang-regex/1.0.0:
resolution: {integrity: sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=} resolution: {integrity: sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
dev: true dev: true
@@ -486,7 +513,7 @@ packages:
resolution: {integrity: sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==} resolution: {integrity: sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==}
dependencies: dependencies:
call-bind: 1.0.2 call-bind: 1.0.2
get-intrinsic: 1.1.1 get-intrinsic: 1.1.2
object-inspect: 1.12.2 object-inspect: 1.12.2
dev: true dev: true
@@ -538,7 +565,7 @@ packages:
dev: true dev: true
/strip-bom/3.0.0: /strip-bom/3.0.0:
resolution: {integrity: sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=} resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==}
engines: {node: '>=4'} engines: {node: '>=4'}
dev: true dev: true

BIN
test/assets/AES.pdf (Stored with Git LFS) Normal file

Binary file not shown.

BIN
test/assets/Pigeons.zip (Stored with Git LFS) Normal file

Binary file not shown.

BIN
test/assets/alfred-kenneally-UIu4RmMxnHU-unsplash.jpg (Stored with Git LFS) Normal file

Binary file not shown.

5
test/file/files.ts Normal file
View File

@@ -0,0 +1,5 @@
export default {
PDF: 'test/assets/AES.pdf',
Image: 'test/assets/alfred-kenneally-UIu4RmMxnHU-unsplash.jpg',
Zip: 'test/assets/Pigeons.zip',
}

View File

@@ -0,0 +1,11 @@
import { test } from '@playwright/test'
import { checkLinkForDownload, createNote, getFileChecksum } from '../utils'
import Files from './files'
test('multiple', async ({ page }) => {
const files = [Files.PDF, Files.Image]
const checksums = await Promise.all(files.map(getFileChecksum))
const link = await createNote(page, { files, views: 2 })
await checkLinkForDownload(page, link, 'alfred-kenneally', checksums[1])
await checkLinkForDownload(page, link, 'AES.pdf', checksums[0])
})

24
test/file/simple.spec.ts Normal file
View File

@@ -0,0 +1,24 @@
import { test } from '@playwright/test'
import { checkLinkDoesNotExist, checkLinkForDownload, checkLinkForText, createNote, getFileChecksum } from '../utils'
import Files from './files'
test('simple pdf', async ({ page }) => {
const files = [Files.PDF]
const link = await createNote(page, { files })
await checkLinkForText(page, link, 'AES.pdf')
await checkLinkDoesNotExist(page, link)
})
test('pdf content', async ({ page }) => {
const files = [Files.PDF]
const checksum = await getFileChecksum(files[0])
const link = await createNote(page, { files })
await checkLinkForDownload(page, link, 'AES.pdf', checksum)
})
test('image content', async ({ page }) => {
const files = [Files.Image]
const checksum = await getFileChecksum(files[0])
const link = await createNote(page, { files })
await checkLinkForDownload(page, link, 'alfred-kenneally', checksum)
})

37
test/text.ts Normal file
View File

@@ -0,0 +1,37 @@
import { expect, test, type Page } from '@playwright/test'
async function createNote(page: Page, text: string): Promise<string> {
await page.goto('/')
await page.locator('textarea').click()
await page.locator('textarea').fill(text)
await page.locator('button:has-text("create")').click()
await page.locator('[data-testid="share-link"]').click()
const shareLink = await page.locator('[data-testid="share-link"]').inputValue()
return shareLink
}
async function checkLinkForText(page: Page, link: string, text: string) {
await page.goto(link)
await page.locator('[data-testid="show-note-button"]').click()
expect(await page.locator('[data-testid="result"] >> .note').innerText()).toBe(text)
}
async function checkLinkDoesNotExist(page: Page, link: string) {
await page.goto('/') // Required due to firefox: https://github.com/microsoft/playwright/issues/15781
await page.goto(link)
await expect(page.locator('main')).toContainText('note was not found or was already deleted')
}
test('simple', async ({ page }) => {
const text = `Endless prejudice endless play derive joy eternal-return selfish burying. Of decieve play pinnacle faith disgust. Spirit reason salvation burying strong of joy ascetic selfish against merciful sea truth. Ubermensch moral prejudice derive chaos mountains ubermensch justice philosophy justice ultimate joy ultimate transvaluation. Virtues convictions war ascetic eternal-return spirit. Ubermensch transvaluation noble revaluation sexuality intentions salvation endless decrepit hope noble fearful. Justice ideal ultimate snare god joy evil sexuality insofar gains oneself ideal.`
const shareLink = await createNote(page, text)
await checkLinkForText(page, shareLink, text)
})
test('only shown once', async ({ page }) => {
const text = `Christian victorious reason suicide dead. Right ultimate gains god hope truth burying selfish society joy. Ultimate.`
const shareLink = await createNote(page, text)
await checkLinkForText(page, shareLink, text)
await checkLinkDoesNotExist(page, shareLink)
})

View File

@@ -0,0 +1,14 @@
import { test } from '@playwright/test'
import { checkLinkDoesNotExist, checkLinkForText, createNote } from '../utils'
test('1 minute', async ({ page }) => {
const text = `Virtues value ascetic revaluation sea dead strong burying.`
const minutes = 1
const timeout = minutes * 60_000
test.setTimeout(timeout * 2)
const shareLink = await createNote(page, { text, expiration: minutes })
await checkLinkForText(page, shareLink, text)
await checkLinkForText(page, shareLink, text)
await page.waitForTimeout(timeout)
await checkLinkDoesNotExist(page, shareLink)
})

8
test/text/simple.spec.ts Normal file
View File

@@ -0,0 +1,8 @@
import { test } from '@playwright/test'
import { checkLinkForText, createNote } from '../utils'
test('simple', async ({ page }) => {
const text = `Endless prejudice endless play derive joy eternal-return selfish burying. Of decieve play pinnacle faith disgust. Spirit reason salvation burying strong of joy ascetic selfish against merciful sea truth. Ubermensch moral prejudice derive chaos mountains ubermensch justice philosophy justice ultimate joy ultimate transvaluation. Virtues convictions war ascetic eternal-return spirit. Ubermensch transvaluation noble revaluation sexuality intentions salvation endless decrepit hope noble fearful. Justice ideal ultimate snare god joy evil sexuality insofar gains oneself ideal.`
const shareLink = await createNote(page, { text })
await checkLinkForText(page, shareLink, text)
})

18
test/text/views.spec.ts Normal file
View File

@@ -0,0 +1,18 @@
import { test } from '@playwright/test'
import { checkLinkDoesNotExist, checkLinkForText, createNote } from '../utils'
test('only shown once', async ({ page }) => {
const text = `Christian victorious reason suicide dead. Right ultimate gains god hope truth burying selfish society joy. Ultimate.`
const shareLink = await createNote(page, { text })
await checkLinkForText(page, shareLink, text)
await checkLinkDoesNotExist(page, shareLink)
})
test('view 3 times', async ({ page }) => {
const text = `Justice holiest overcome fearful strong ultimate holiest christianity.`
const shareLink = await createNote(page, { text, views: 3 })
await checkLinkForText(page, shareLink, text)
await checkLinkForText(page, shareLink, text)
await checkLinkForText(page, shareLink, text)
await checkLinkDoesNotExist(page, shareLink)
})

71
test/utils.ts Normal file
View File

@@ -0,0 +1,71 @@
import { expect, type Page } from '@playwright/test'
import { createHash } from 'crypto'
import { readFile } from 'fs/promises'
type CreatePage = { text?: string; files?: string[]; views?: number; expiration?: number; error?: string }
export async function createNote(page: Page, options: CreatePage): Promise<string> {
await page.goto('/')
if (options.text) {
await page.locator('[data-testid="text-field"]').fill(options.text)
} else if (options.files) {
await page.locator('[data-testid="switch-file"]').click()
const [fileChooser] = await Promise.all([
page.waitForEvent('filechooser'),
page.locator('text=No Files Selected').click(),
])
await fileChooser.setFiles(options.files)
}
if (options.views) {
await page.locator('[data-testid="switch-advanced"]').click()
await page.locator('[data-testid="field-views"]').fill(options.views.toString())
} else if (options.expiration) {
await page.locator('[data-testid="switch-advanced"]').click()
await page.locator('[data-testid="switch-advanced-toggle"]').click()
await page.locator('[data-testid="field-expiration"]').fill(options.expiration.toString())
}
await page.locator('button:has-text("create")').click()
if (options.error) {
await expect(page.locator('.error-text')).toContainText(options.error, { timeout: 60_000 })
}
const shareLink = await page.locator('[data-testid="share-link"]').inputValue()
return shareLink
}
export async function checkLinkForDownload(page: Page, link: string, text: string, checksum: string) {
await page.goto('/')
await page.goto(link)
await page.locator('[data-testid="show-note-button"]').click()
const [download] = await Promise.all([
page.waitForEvent('download'),
page.locator(`[data-testid="result"] >> text=${text}`).click(),
])
const path = await download.path()
if (!path) throw new Error('Download failed')
const cs = await getFileChecksum(path)
await expect(cs).toBe(checksum)
}
export async function checkLinkForText(page: Page, link: string, text: string) {
await page.goto('/')
await page.goto(link)
await page.locator('[data-testid="show-note-button"]').click()
await expect(await page.locator('[data-testid="result"] >> .note').innerText()).toContain(text)
}
export async function checkLinkDoesNotExist(page: Page, link: string) {
await page.goto('/') // Required due to firefox: https://github.com/microsoft/playwright/issues/15781
await page.goto(link)
await expect(page.locator('main')).toContainText('note was not found or was already deleted')
}
export async function getFileChecksum(file: string) {
const buffer = await readFile(file)
const hash = createHash('sha3-256').update(buffer).digest('hex')
return hash
}