feat: added navigation of uploaded files and browsing of inner sites; refactor: HTML/CSS/JS is now templated;
This commit is contained in:
parent
e460f88a7f
commit
f94ec335dd
214
Cargo.lock
generated
214
Cargo.lock
generated
@ -26,6 +26,21 @@ dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "alloc-no-stdlib"
|
||||
version = "2.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3"
|
||||
|
||||
[[package]]
|
||||
name = "alloc-stdlib"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "94fb8275041c72129eb51b7d0322c29b8387a0386127718b096429201a5d6ece"
|
||||
dependencies = [
|
||||
"alloc-no-stdlib",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anyhow"
|
||||
version = "1.0.98"
|
||||
@ -59,7 +74,7 @@ dependencies = [
|
||||
"rustc-hash",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"syn 2.0.100",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -74,6 +89,20 @@ dependencies = [
|
||||
"winnow",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-compression"
|
||||
version = "0.4.23"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b37fc50485c4f3f736a4fb14199f6d5f5ba008d7f28fe710306c92780f004c07"
|
||||
dependencies = [
|
||||
"brotli",
|
||||
"flate2",
|
||||
"futures-core",
|
||||
"memchr",
|
||||
"pin-project-lite",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "atomic-waker"
|
||||
version = "1.1.2"
|
||||
@ -88,9 +117,9 @@ checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26"
|
||||
|
||||
[[package]]
|
||||
name = "axum"
|
||||
version = "0.8.1"
|
||||
version = "0.8.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6d6fd624c75e18b3b4c6b9caf42b1afe24437daaee904069137d8bab077be8b8"
|
||||
checksum = "021e862c184ae977658b36c4500f7feac3221ca5da43e3f25bd04ab6c79a29b5"
|
||||
dependencies = [
|
||||
"axum-core",
|
||||
"bytes",
|
||||
@ -122,13 +151,24 @@ dependencies = [
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "axum-core"
|
||||
version = "0.5.0"
|
||||
name = "axum-auth"
|
||||
version = "0.8.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "df1362f362fd16024ae199c1970ce98f9661bf5ef94b9808fee734bc3698b733"
|
||||
checksum = "93495037c01c639b198ecb926b58f7f1c0d61ae663edcd61b2dd679f2a0bffe6"
|
||||
dependencies = [
|
||||
"axum-core",
|
||||
"base64",
|
||||
"http",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "axum-core"
|
||||
version = "0.5.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "68464cd0412f486726fb3373129ef5d2993f90c34bc2bc1c1e9943b2f4fc7ca6"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"futures-util",
|
||||
"futures-core",
|
||||
"http",
|
||||
"http-body",
|
||||
"http-body-util",
|
||||
@ -141,6 +181,29 @@ dependencies = [
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "axum-extra"
|
||||
version = "0.10.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "45bf463831f5131b7d3c756525b305d40f1185b688565648a92e1392ca35713d"
|
||||
dependencies = [
|
||||
"axum",
|
||||
"axum-core",
|
||||
"bytes",
|
||||
"cookie",
|
||||
"futures-util",
|
||||
"http",
|
||||
"http-body",
|
||||
"http-body-util",
|
||||
"mime",
|
||||
"pin-project-lite",
|
||||
"rustversion",
|
||||
"serde",
|
||||
"tower",
|
||||
"tower-layer",
|
||||
"tower-service",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "backtrace"
|
||||
version = "0.3.74"
|
||||
@ -177,6 +240,27 @@ version = "2.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd"
|
||||
|
||||
[[package]]
|
||||
name = "brotli"
|
||||
version = "8.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9991eea70ea4f293524138648e41ee89b0b2b12ddef3b255effa43c8056e0e0d"
|
||||
dependencies = [
|
||||
"alloc-no-stdlib",
|
||||
"alloc-stdlib",
|
||||
"brotli-decompressor",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "brotli-decompressor"
|
||||
version = "5.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "874bb8112abecc98cbd6d81ea4fa7e94fb9449648c93cc89aa40c81c24d7de03"
|
||||
dependencies = [
|
||||
"alloc-no-stdlib",
|
||||
"alloc-stdlib",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bumpalo"
|
||||
version = "3.17.0"
|
||||
@ -189,6 +273,12 @@ version = "1.10.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a"
|
||||
|
||||
[[package]]
|
||||
name = "bytesize"
|
||||
version = "2.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a3c8f83209414aacf0eeae3cf730b18d6981697fba62f200fcfb92b9f082acba"
|
||||
|
||||
[[package]]
|
||||
name = "cc"
|
||||
version = "1.2.19"
|
||||
@ -204,6 +294,17 @@ version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
|
||||
|
||||
[[package]]
|
||||
name = "cookie"
|
||||
version = "0.18.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747"
|
||||
dependencies = [
|
||||
"percent-encoding",
|
||||
"time",
|
||||
"version_check",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "core-foundation"
|
||||
version = "0.9.4"
|
||||
@ -220,6 +321,15 @@ version = "0.8.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
|
||||
|
||||
[[package]]
|
||||
name = "crc32fast"
|
||||
version = "1.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam-channel"
|
||||
version = "0.5.15"
|
||||
@ -252,7 +362,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.100",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -264,16 +374,6 @@ dependencies = [
|
||||
"cfg-if",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "env_filter"
|
||||
version = "0.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "186e05a59d4c50738528153b83b0b0194d3a29507dfec16eccd4b342903397d0"
|
||||
dependencies = [
|
||||
"log",
|
||||
"regex",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "equivalent"
|
||||
version = "1.0.2"
|
||||
@ -296,6 +396,16 @@ version = "2.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be"
|
||||
|
||||
[[package]]
|
||||
name = "flate2"
|
||||
version = "1.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7ced92e76e966ca2fd84c8f7aa01a4aea65b0eb6648d72f7c8f3e2764a67fece"
|
||||
dependencies = [
|
||||
"crc32fast",
|
||||
"miniz_oxide",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fnv"
|
||||
version = "1.0.7"
|
||||
@ -659,7 +769,7 @@ checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.100",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -890,7 +1000,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.100",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -924,10 +1034,10 @@ dependencies = [
|
||||
"anyhow",
|
||||
"askama",
|
||||
"axum",
|
||||
"axum-auth",
|
||||
"axum-extra",
|
||||
"bytes",
|
||||
"env_filter",
|
||||
"log",
|
||||
"mime",
|
||||
"bytesize",
|
||||
"rand",
|
||||
"reqwest",
|
||||
"serde",
|
||||
@ -938,11 +1048,11 @@ dependencies = [
|
||||
"tokio",
|
||||
"tower",
|
||||
"tower-http",
|
||||
"trace",
|
||||
"tower-service",
|
||||
"tracing",
|
||||
"tracing-appender",
|
||||
"tracing-subscriber",
|
||||
"url",
|
||||
"urlencoding",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -1264,7 +1374,7 @@ checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.100",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -1400,17 +1510,6 @@ version = "2.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "1.0.109"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.100"
|
||||
@ -1439,7 +1538,7 @@ checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.100",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -1502,7 +1601,7 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.100",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -1513,7 +1612,7 @@ checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.100",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -1591,7 +1690,7 @@ checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.100",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -1649,8 +1748,10 @@ version = "0.6.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "403fa3b783d4b626a8ad51d766ab03cb6d2dbfc46b1c5d4448395e6628dc9697"
|
||||
dependencies = [
|
||||
"async-compression",
|
||||
"bitflags",
|
||||
"bytes",
|
||||
"futures-core",
|
||||
"futures-util",
|
||||
"http",
|
||||
"http-body",
|
||||
@ -1680,17 +1781,6 @@ version = "0.3.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3"
|
||||
|
||||
[[package]]
|
||||
name = "trace"
|
||||
version = "0.1.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9ad0c048e114d19d1140662762bfdb10682f3bc806d8be18af846600214dd9af"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 1.0.109",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tracing"
|
||||
version = "0.1.41"
|
||||
@ -1723,7 +1813,7 @@ checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.100",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -1819,6 +1909,12 @@ dependencies = [
|
||||
"percent-encoding",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "urlencoding"
|
||||
version = "2.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da"
|
||||
|
||||
[[package]]
|
||||
name = "utf16_iter"
|
||||
version = "1.0.5"
|
||||
@ -1895,7 +1991,7 @@ dependencies = [
|
||||
"log",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.100",
|
||||
"syn",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
|
||||
@ -1930,7 +2026,7 @@ checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.100",
|
||||
"syn",
|
||||
"wasm-bindgen-backend",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
@ -2207,7 +2303,7 @@ checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.100",
|
||||
"syn",
|
||||
"synstructure",
|
||||
]
|
||||
|
||||
@ -2228,7 +2324,7 @@ checksum = "a996a8f63c5c4448cd959ac1bab0aaa3306ccfd060472f85943ee0750f0169be"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.100",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -2248,7 +2344,7 @@ checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.100",
|
||||
"syn",
|
||||
"synstructure",
|
||||
]
|
||||
|
||||
@ -2277,5 +2373,5 @@ checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.100",
|
||||
"syn",
|
||||
]
|
||||
|
||||
12
Cargo.toml
12
Cargo.toml
@ -8,10 +8,10 @@ authors = ["Kenneth Jao"]
|
||||
anyhow = "1.0.98"
|
||||
askama = { version = "0.14.0", features = ["blocks"] }
|
||||
axum = { version = "0.8.1", features = ["multipart"] }
|
||||
axum-auth = "0.8.1"
|
||||
axum-extra = { version = "0.10.1", features = ["cookie"] }
|
||||
bytes = "1.10.1"
|
||||
env_filter = "0.1.3"
|
||||
log = "0.4.27"
|
||||
mime = "0.3.17"
|
||||
bytesize = "2.0.1"
|
||||
rand = { version = "0.9.1", features = ["alloc"] }
|
||||
reqwest = { version = "0.12.15", features = ["json"] }
|
||||
serde = { version= "1.0.219", features = ["derive"] }
|
||||
@ -21,9 +21,9 @@ sqlite = "0.37.0"
|
||||
thiserror = "2.0.12"
|
||||
tokio = { version = "1.44.1", features = ["macros", "rt-multi-thread"] }
|
||||
tower = "0.5.2"
|
||||
tower-http = { version = "0.6.2", features = ["fs", "trace"] }
|
||||
trace = "0.1.7"
|
||||
tower-http = { version = "0.6.2", features = ["compression-br", "compression-gzip", "fs", "trace"] }
|
||||
tower-service = "0.3.3"
|
||||
tracing = "0.1.41"
|
||||
tracing-appender = "0.2.3"
|
||||
tracing-subscriber = { version = "0.3.19", features = ["env-filter", "json"] }
|
||||
url = "2.5.4"
|
||||
urlencoding = "2.1.3"
|
||||
|
||||
46
README.md
46
README.md
@ -1,6 +1,46 @@
|
||||
# pack
|
||||
|
||||
A web server written in [rust](https://www.rust-lang.org/) with [axum](https://docs.rs/axum/latest/axum/) for hosting built packages as a
|
||||
destination endpoint for packages in a continuous integration workflow. It is designed around Gitea, using it for its authentication
|
||||
as well as integrating with Gitea actions.
|
||||
Pack is web server written in [rust](https://www.rust-lang.org/) with
|
||||
[axum](https://docs.rs/axum/latest/axum/) for hosting built packages as a
|
||||
destination endpoint for packages in a continuous integration workflow. It is
|
||||
designed around Gitea, using it for its authentication as well as integrating
|
||||
with Gitea actions.
|
||||
|
||||
## Setup
|
||||
To setup the server, rename `pack.yml.template` to `pack.yml` and fill in the details
|
||||
for your configuration. Place the built executable in a folder with properly configured
|
||||
permissions, and make sure the upload path has permissions `750`. Indeed, all
|
||||
subdirectories are created with this permissions, and all files are created with
|
||||
`640`.
|
||||
|
||||
|
||||
## API
|
||||
The only API endpoint intended to be exposed is `/api/files/{owner}/{repo}/{*path}`, which
|
||||
supports `GET` and `DELETE`. Every request requires an Bearer Authorization token,
|
||||
which should be exactly the Gitea OAuth2 token. This API is intended for scripted management of uploaded
|
||||
packages in the contiguous integration (CI) chain. For instance, automatically pulling
|
||||
latest versions into a production environment.
|
||||
|
||||
### GET
|
||||
In the case, of GET, a JSON of the entries in a directory is returned. The format
|
||||
is shown below.
|
||||
|
||||
```json
|
||||
{
|
||||
"entries": [
|
||||
{
|
||||
name: "...",
|
||||
name_uri: "...",
|
||||
icon: "...",
|
||||
size: "...",
|
||||
},
|
||||
...
|
||||
]
|
||||
```
|
||||
|
||||
If the requested path is a file, it will redirect to download the file. If using `curl`,
|
||||
make sure the `-L` flag is enabled to follow redirects.
|
||||
|
||||
### DELETE
|
||||
This will delete the file at the destination, or delete the *entire* directory.
|
||||
Use with caution.
|
||||
|
||||
411
src/api.rs
411
src/api.rs
@ -1,22 +1,42 @@
|
||||
use std::{fs, fmt, path, slice::Iter};
|
||||
use std::{
|
||||
cmp::Ordering,
|
||||
fs::{self, Permissions},
|
||||
fmt,
|
||||
os::unix::fs::PermissionsExt,
|
||||
path,
|
||||
slice::Iter,
|
||||
time::{Duration},
|
||||
};
|
||||
use bytes::Bytes;
|
||||
use bytesize::ByteSize;
|
||||
use std::process::Command;
|
||||
use rand::distr::{Alphanumeric, SampleString};
|
||||
use thiserror::Error;
|
||||
use axum::{
|
||||
body::Body,
|
||||
extract::{Path, Json, State, Multipart},
|
||||
http::{StatusCode, header::{HeaderMap, AUTHORIZATION, CONTENT_TYPE}},
|
||||
response::{IntoResponse, Response},
|
||||
http::{
|
||||
StatusCode,
|
||||
header::{HeaderMap, CONTENT_TYPE, AUTHORIZATION},
|
||||
Uri,
|
||||
},
|
||||
response::{IntoResponse, Response, Redirect, Html},
|
||||
middleware::Next,
|
||||
};
|
||||
use axum_auth::AuthBearer;
|
||||
use axum_extra::extract::CookieJar;
|
||||
use reqwest::{
|
||||
Client, Method, Request, RequestBuilder, Url
|
||||
};
|
||||
use serde::{Serialize, Deserialize};
|
||||
use serde_json::Value;
|
||||
use tower_http::services::ServeFile;
|
||||
|
||||
use crate::{query, ServerState};
|
||||
|
||||
static FOLDER_PERM: u32 = 0o750;
|
||||
static FILE_PERM: u32 = 0o640;
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
#[repr(u8)]
|
||||
pub enum RepoFeature {
|
||||
@ -48,7 +68,7 @@ impl From<RepoFeature> for String {
|
||||
}
|
||||
|
||||
impl TryFrom<&str> for RepoFeature {
|
||||
type Error = APIError;
|
||||
type Error = ApiError;
|
||||
|
||||
fn try_from(value: &str) -> Result<Self> {
|
||||
for feature in RepoFeature::iter() {
|
||||
@ -56,13 +76,30 @@ impl TryFrom<&str> for RepoFeature {
|
||||
return Ok(*feature);
|
||||
}
|
||||
}
|
||||
Err(APIError::InvalidFeature { feature: value.to_owned() })
|
||||
Err(ApiError::InvalidFeature { feature: value.to_owned() })
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
#[repr(u16)]
|
||||
pub enum ErrorCode {
|
||||
InvalidToken = 0, // Invalid OAuth token.
|
||||
External = 1, // Error originates from an external location/server.
|
||||
Filesystem = 2, // Error originates from filesystem operation.
|
||||
Sql = 3, // Error originates from SQL.
|
||||
BadRequest = 4, // Error originates from client request.
|
||||
}
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum APIError {
|
||||
#[derive(Clone, Copy, Debug, Error)]
|
||||
pub struct CustomError(&'static str, ErrorCode);
|
||||
impl fmt::Display for CustomError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(f, "{}, {:?}", self.0, self.1)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum ApiError {
|
||||
#[error("Request error: {0}")]
|
||||
RequestError(#[from] reqwest::Error),
|
||||
#[error("SQL error: {0}")]
|
||||
@ -81,18 +118,18 @@ pub enum APIError {
|
||||
Unauthorized { repo: String },
|
||||
#[error("A token was not provided or is malformed")]
|
||||
Tokenless,
|
||||
#[error("{msg}")]
|
||||
Other { msg: String },
|
||||
#[error("{0}")]
|
||||
CustomError(#[from] CustomError)
|
||||
}
|
||||
|
||||
#[repr(u16)]
|
||||
pub enum ErrorCode {
|
||||
InvalidToken = 0, // Invalid OAuth token.
|
||||
External = 1, // Error originates from an external location/server.
|
||||
Filesystem = 2, // Error originates from filesystem operation.
|
||||
Sql = 3, // Error originates from SQL.
|
||||
BadRequest = 4, // Error originates from client request.
|
||||
}
|
||||
static ERR_GITEA_CONTENT_TYPE: CustomError = CustomError("Content-Type in Gitea response invalid", ErrorCode::External);
|
||||
static ERR_MISSING_PARENT: CustomError = CustomError("Could not find parent in repo directory. Should be impossible...", ErrorCode::Filesystem);
|
||||
static ERR_UNTAR: CustomError = CustomError("Failed to complete untar", ErrorCode::Filesystem);
|
||||
static ERR_OS_STR_UTF: CustomError = CustomError("OsString failed to parse into UTF-8", ErrorCode::Filesystem);
|
||||
static ERR_FILE_CALL_UTF: CustomError = CustomError("file command output failed to parse into UTF-8", ErrorCode::Filesystem);
|
||||
static ERR_FILE_CALL_UNEXPECTED: CustomError = CustomError("file command output had unexpected output", ErrorCode::Filesystem);
|
||||
pub static ERR_CONTENT_FOLDER_RENDER: CustomError = CustomError("Unable to process 'ContentFolder' template", ErrorCode::Filesystem);
|
||||
static ERR_IMPOSSIBLE_ENTRY: CustomError = CustomError("Entry is neither file, directory or symlink", ErrorCode::Filesystem);
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct ErrorResponse {
|
||||
@ -101,11 +138,11 @@ pub struct ErrorResponse {
|
||||
message: String,
|
||||
}
|
||||
|
||||
type Result<T, E = APIError> = core::result::Result<T, E>;
|
||||
impl IntoResponse for APIError {
|
||||
pub type Result<T, E = ApiError> = core::result::Result<T, E>;
|
||||
impl IntoResponse for ApiError {
|
||||
fn into_response(self) -> Response<Body> {
|
||||
let (status, code) = match self {
|
||||
APIError::RequestError(ref err) => {
|
||||
ApiError::RequestError(ref err) => {
|
||||
let status = err.status().unwrap_or(StatusCode::INTERNAL_SERVER_ERROR);
|
||||
let action = match status {
|
||||
StatusCode::FORBIDDEN => ErrorCode::InvalidToken,
|
||||
@ -113,15 +150,15 @@ impl IntoResponse for APIError {
|
||||
};
|
||||
(status, action)
|
||||
},
|
||||
APIError::SQLError(_) => (StatusCode::INTERNAL_SERVER_ERROR, ErrorCode::Sql),
|
||||
APIError::IOError(_) => (StatusCode::INTERNAL_SERVER_ERROR, ErrorCode::Filesystem),
|
||||
APIError::MultipartError(_) => (StatusCode::INTERNAL_SERVER_ERROR, ErrorCode::BadRequest),
|
||||
APIError::InvalidJson{..} => (StatusCode::INTERNAL_SERVER_ERROR, ErrorCode::External),
|
||||
APIError::InvalidFeature{..} => (StatusCode::BAD_REQUEST, ErrorCode::BadRequest),
|
||||
APIError::DisabledFeature{..} => (StatusCode::NOT_FOUND, ErrorCode::BadRequest),
|
||||
APIError::Unauthorized{..} => (StatusCode::FORBIDDEN, ErrorCode::BadRequest),
|
||||
APIError::Tokenless => (StatusCode::FORBIDDEN, ErrorCode::InvalidToken),
|
||||
APIError::Other{..} => (StatusCode::INTERNAL_SERVER_ERROR, ErrorCode::External),
|
||||
ApiError::SQLError(_) => (StatusCode::INTERNAL_SERVER_ERROR, ErrorCode::Sql),
|
||||
ApiError::IOError(_) => (StatusCode::INTERNAL_SERVER_ERROR, ErrorCode::Filesystem),
|
||||
ApiError::MultipartError(_) => (StatusCode::INTERNAL_SERVER_ERROR, ErrorCode::BadRequest),
|
||||
ApiError::InvalidJson{..} => (StatusCode::INTERNAL_SERVER_ERROR, ErrorCode::External),
|
||||
ApiError::InvalidFeature{..} => (StatusCode::BAD_REQUEST, ErrorCode::BadRequest),
|
||||
ApiError::DisabledFeature{..} => (StatusCode::NOT_FOUND, ErrorCode::BadRequest),
|
||||
ApiError::Unauthorized{..} => (StatusCode::FORBIDDEN, ErrorCode::BadRequest),
|
||||
ApiError::Tokenless => (StatusCode::FORBIDDEN, ErrorCode::InvalidToken),
|
||||
ApiError::CustomError(ref e) => (StatusCode::INTERNAL_SERVER_ERROR, e.1),
|
||||
};
|
||||
(status, Json(ErrorResponse{
|
||||
status: u16::from(status),
|
||||
@ -160,7 +197,7 @@ async fn gitea_api<T, U>(url: &str, method: Method, payload: &T, token: &str) ->
|
||||
content_type = res.headers().get(CONTENT_TYPE)
|
||||
.unwrap()
|
||||
.to_str()
|
||||
.map_err(|_| APIError::Other { msg: "Content-Type in Gitea response invalid".to_owned() })?
|
||||
.map_err(|_| ERR_GITEA_CONTENT_TYPE )?
|
||||
.to_owned();
|
||||
}
|
||||
|
||||
@ -171,7 +208,7 @@ async fn gitea_api<T, U>(url: &str, method: Method, payload: &T, token: &str) ->
|
||||
}
|
||||
}
|
||||
|
||||
async fn authorize(host: &str, token: &str, repo: &str) -> Result<Json<Value>> {
|
||||
async fn authorize(host: &str, token: &str, repo: &str, kind: &str) -> Result<Json<Value>> {
|
||||
// Use repos API call to check admin permission. Return the repo JSON as
|
||||
// well in case it needs to be used later.
|
||||
let url = format!("{host}/api/v1/repos/{repo}");
|
||||
@ -179,15 +216,15 @@ async fn authorize(host: &str, token: &str, repo: &str) -> Result<Json<Value>> {
|
||||
|
||||
let json: Value = gitea_api(&url, Method::GET, &data, token).await?;
|
||||
|
||||
// If permission is not admin level, return error.
|
||||
// If 'kind' permission is not true, return error.
|
||||
json.get("permissions")
|
||||
.ok_or(APIError::InvalidJson{ msg: "Couldn't find 'permissions' key.".to_owned() })?
|
||||
.get("admin")
|
||||
.ok_or(APIError::InvalidJson{ msg: "Couldn't find 'admin' key.".to_owned() })?
|
||||
.ok_or(ApiError::InvalidJson{ msg: "Couldn't find 'permissions' key.".to_owned() })?
|
||||
.get(kind)
|
||||
.ok_or(ApiError::InvalidJson{ msg: format!("Couldn't find '{kind}' key.") })?
|
||||
.as_bool()
|
||||
.ok_or(APIError::InvalidJson{ msg: "Value in 'admin' is not bool.".to_owned() })?
|
||||
.ok_or(ApiError::InvalidJson{ msg: format!("Value in '{kind}' is not bool.") })?
|
||||
.then_some(0)
|
||||
.ok_or(APIError::Unauthorized{ repo: repo.to_owned() })?;
|
||||
.ok_or(ApiError::Unauthorized{ repo: repo.to_owned() })?;
|
||||
|
||||
Ok(Json(json))
|
||||
}
|
||||
@ -264,26 +301,17 @@ pub struct RepoResponse {
|
||||
features: FeatureList,
|
||||
}
|
||||
|
||||
fn extract_token(headers: HeaderMap) -> Result<String> {
|
||||
Ok(headers.get(AUTHORIZATION)
|
||||
.ok_or(APIError::Tokenless)?
|
||||
.to_str()
|
||||
.map_err(|_| APIError::Tokenless)?
|
||||
.to_owned())
|
||||
}
|
||||
|
||||
pub async fn get_repo(State(state): State<ServerState<'_>>,
|
||||
Path((owner, repo)): Path<(String, String)>,
|
||||
headers: HeaderMap) -> Result<Json<RepoResponse>> {
|
||||
AuthBearer(token): AuthBearer) -> Result<Json<RepoResponse>> {
|
||||
let repo = format!("{owner}/{repo}");
|
||||
let token = extract_token(headers)?;
|
||||
// Pull repository information from Gitea.
|
||||
let json = authorize(&state.config.gitea_host, &token, &repo).await?;
|
||||
let json = authorize(&state.config.gitea_host, &token, &repo, "admin").await?;
|
||||
|
||||
let description = json.get("description")
|
||||
.ok_or(APIError::InvalidJson{ msg: "Couldn't find 'description' key.".to_owned() })?
|
||||
.ok_or(ApiError::InvalidJson{ msg: "Couldn't find 'description' key.".to_owned() })?
|
||||
.as_str()
|
||||
.ok_or(APIError::InvalidJson{ msg: "Value in 'description' is not String.".to_owned() })?
|
||||
.ok_or(ApiError::InvalidJson{ msg: "Value in 'description' is not String.".to_owned() })?
|
||||
.to_owned();
|
||||
|
||||
// Check if entry exists and return features.
|
||||
@ -309,11 +337,10 @@ pub struct GiteaSetSecret {
|
||||
|
||||
pub async fn create_repo(State(state): State<ServerState<'_>>,
|
||||
Path((owner, repo)): Path<(String, String)>,
|
||||
headers: HeaderMap,
|
||||
AuthBearer(token): AuthBearer,
|
||||
) -> Result<()> {
|
||||
let repo = format!("{owner}/{repo}").to_lowercase(); // Repos are case-insensitive.
|
||||
let token = extract_token(headers)?;
|
||||
let _ = authorize(&state.config.gitea_host, &token, &repo).await?;
|
||||
let _ = authorize(&state.config.gitea_host, &token, &repo, "admin").await?;
|
||||
|
||||
// Create secret and insert new repository into database.
|
||||
let secret = Alphanumeric.sample_string(&mut rand::rng(), 48);
|
||||
@ -325,10 +352,20 @@ pub async fn create_repo(State(state): State<ServerState<'_>>,
|
||||
(3, secret.clone().into()),
|
||||
])?
|
||||
.map(|_| 0)
|
||||
.sum(); // Evalute statement.
|
||||
.sum(); // Evaluate statement.
|
||||
|
||||
// Make associated folder.
|
||||
fs::create_dir_all(path::Path::new(&state.config.upload_path).join(&repo))?;
|
||||
let owner_dir = path::Path::new(&state.config.upload_path).join(&owner);
|
||||
let repo_dir = path::Path::new(&state.config.upload_path).join(&repo);
|
||||
if !fs::exists(&owner_dir)? {
|
||||
fs::create_dir(&owner_dir)?;
|
||||
fs::set_permissions(owner_dir, Permissions::from_mode(FOLDER_PERM))?;
|
||||
}
|
||||
|
||||
if !fs::exists(&repo_dir)? {
|
||||
fs::create_dir(&repo_dir)?;
|
||||
fs::set_permissions(repo_dir, Permissions::from_mode(FOLDER_PERM))?;
|
||||
}
|
||||
|
||||
// Add secret to Gitea secrets.
|
||||
let secret_url = format!("{}/api/v1/repos/{}/actions/secrets/PACK_REPO_SECRET", &state.config.gitea_host, &repo);
|
||||
@ -340,23 +377,22 @@ pub async fn create_repo(State(state): State<ServerState<'_>>,
|
||||
}
|
||||
pub async fn delete_repo(State(state): State<ServerState<'_>>,
|
||||
Path((owner, repo)): Path<(String, String)>,
|
||||
headers: HeaderMap,
|
||||
AuthBearer(token): AuthBearer,
|
||||
) -> Result<()> {
|
||||
let repo = format!("{owner}/{repo}").to_lowercase(); // Repos are case-insensitive.
|
||||
let token = extract_token(headers)?;
|
||||
let _ = authorize(&state.config.gitea_host, &token, &repo).await?;
|
||||
let _ = authorize(&state.config.gitea_host, &token, &repo, "admin").await?;
|
||||
|
||||
let _: i32 = state.sql.prepare(crate::query::DELETE_REPO)?
|
||||
.iter()
|
||||
.bind((1, repo.as_str()))?
|
||||
.map(|_| 0)
|
||||
.sum(); // Evalute statement.
|
||||
.sum(); // Evaluate statement.
|
||||
|
||||
// Remove entire directory and its parent if its empty.
|
||||
let dir = path::Path::new(&state.config.upload_path).join(&repo);
|
||||
let parent = dir.as_path()
|
||||
.parent()
|
||||
.ok_or(APIError::Other { msg: "Could not find parent in repo directory. Should be impossible...".to_owned() })?;
|
||||
.ok_or(ERR_MISSING_PARENT)?;
|
||||
|
||||
fs::remove_dir_all(&dir)?;
|
||||
if fs::read_dir(parent)?.next().is_none() {
|
||||
@ -385,7 +421,7 @@ async fn update_secret(state: &ServerState<'_>, repo: &str, token: &str) -> Resu
|
||||
(2, repo.into()),
|
||||
])?
|
||||
.map(|_| 0)
|
||||
.sum(); // Evalute statement.
|
||||
.sum(); // Evaluate statement.
|
||||
|
||||
let secret_url = format!("{}/api/v1/repos/{}/actions/secrets/PACK_REPO_SECRET", &state.config.gitea_host, repo);
|
||||
let data = GiteaSetSecret { data: secret };
|
||||
@ -406,13 +442,14 @@ async fn update_feature(state: &ServerState<'_>, repo: &str, feature: &str) -> R
|
||||
.map(|row| -> Result<u64> {
|
||||
Ok(row?.read::<i64, _>("features") as u64)
|
||||
})
|
||||
.collect::<Result<Vec<u64>>>()?[0]; // Evalute statement.
|
||||
.collect::<Result<Vec<u64>>>()?[0]; // Evaluate statement.
|
||||
|
||||
// Check added or removed and update folders accordingly.
|
||||
let added = (features & feat_num) == feat_num;
|
||||
let dir = path::Path::new(&state.config.upload_path).join(repo).join(feature);
|
||||
if added {
|
||||
fs::create_dir(dir)?;
|
||||
fs::create_dir(&dir)?;
|
||||
fs::set_permissions(dir, Permissions::from_mode(FOLDER_PERM))?;
|
||||
} else {
|
||||
fs::remove_dir_all(dir)?;
|
||||
}
|
||||
@ -421,11 +458,10 @@ async fn update_feature(state: &ServerState<'_>, repo: &str, feature: &str) -> R
|
||||
|
||||
pub async fn patch_repo(State(state): State<ServerState<'_>>,
|
||||
Path((owner, repo)): Path<(String, String)>,
|
||||
headers: HeaderMap,
|
||||
AuthBearer(token): AuthBearer,
|
||||
Json(payload): Json<PatchRepoRequest>) -> Result<()> {
|
||||
let repo = format!("{owner}/{repo}").to_lowercase(); // Repos are case-insensitive.
|
||||
let token = extract_token(headers)?;
|
||||
let _ = authorize(&state.config.gitea_host, &token, &repo).await?;
|
||||
let _ = authorize(&state.config.gitea_host, &token, &repo, "admin").await?;
|
||||
|
||||
if payload.secret {
|
||||
return update_secret(&state, &repo, &token).await;
|
||||
@ -433,6 +469,119 @@ pub async fn patch_repo(State(state): State<ServerState<'_>>,
|
||||
update_feature(&state, &repo, &payload.feature).await
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct Entry {
|
||||
pub name: String,
|
||||
pub name_uri: String,
|
||||
pub icon: &'static str,
|
||||
pub size: String,
|
||||
}
|
||||
|
||||
fn entry_from_dir_entry(entry: std::io::Result<fs::DirEntry>) -> Result<Entry> {
|
||||
let path = entry?.path();
|
||||
let name = path.file_name().unwrap().to_str().ok_or(ERR_OS_STR_UTF)?.to_owned();
|
||||
let name_uri = urlencoding::encode(&name).to_string();
|
||||
let metadata = path.metadata()?;
|
||||
|
||||
if metadata.is_dir() {
|
||||
return Ok(Entry{
|
||||
name, name_uri, icon: "folder", size: "—".to_owned(),
|
||||
});
|
||||
}
|
||||
let size = ByteSize::b(metadata.len()).display().iec().to_string();
|
||||
|
||||
// Get file mime data.
|
||||
let out = Command::new("file").args(["-b", "--mime", path.to_str().ok_or(ERR_OS_STR_UTF)?]).output()?;
|
||||
let out = String::from_utf8(out.stdout).map_err(|_| ERR_FILE_CALL_UTF)?;
|
||||
let out = out.trim().split("; ").collect::<Vec<_>>();
|
||||
|
||||
// Example expected output: text/plain; charset=us=ascii
|
||||
(out.len() == 2).then_some(0).ok_or(ERR_FILE_CALL_UNEXPECTED)?;
|
||||
let mime = out[0];
|
||||
let charset = &out[1][8..];
|
||||
|
||||
let icon = if &mime[..4] == "text" {
|
||||
"text"
|
||||
} else if charset == "binary" {
|
||||
match mime {
|
||||
"application/gzip" => "archive",
|
||||
"application/zip" => "archive",
|
||||
_ => "binary"
|
||||
}
|
||||
} else {
|
||||
""
|
||||
};
|
||||
|
||||
Ok(Entry{ name, name_uri, icon, size })
|
||||
}
|
||||
|
||||
fn sort_entry(a: &Entry, b: &Entry) -> Ordering {
|
||||
// Sort list by alphabetical folder lowest (so it appears at the top).
|
||||
let (a_fol, b_fol) = (a.icon == "folder", b.icon == "folder");
|
||||
match (a_fol, b_fol) {
|
||||
(true, true) => a.name.to_lowercase().cmp(&b.name.to_lowercase()),
|
||||
(true, false) => Ordering::Less,
|
||||
(false, true) => Ordering::Greater,
|
||||
(false, false) => a.name.to_lowercase().cmp(&b.name.to_lowercase()),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct GetEntryResponse {
|
||||
entries: Vec<Entry>,
|
||||
}
|
||||
|
||||
pub async fn get_entry(State(state): State<ServerState<'_>>,
|
||||
Path((owner, repo, path)): Path<(String, String, String)>,
|
||||
AuthBearer(token): AuthBearer) -> Result<Response> {
|
||||
let repo = format!("{owner}/{repo}").to_lowercase(); // Repos are case-insensitive.
|
||||
let target = format!("{repo}/{path}");
|
||||
|
||||
let _ = authorize(&state.config.gitea_host, &token, &repo, "pull").await?;
|
||||
|
||||
let dir = path::Path::new(&state.config.upload_path).join(&target);
|
||||
|
||||
let metadata = match fs::metadata(&dir) {
|
||||
Ok(x) => x,
|
||||
Err(_) => return Ok(StatusCode::NOT_FOUND.into_response()),
|
||||
};
|
||||
|
||||
if metadata.is_dir() { // Return JSON.
|
||||
let dir = fs::read_dir(&dir)?;
|
||||
let mut entries = dir.map(entry_from_dir_entry).collect::<Result<Vec<Entry>>>()?;
|
||||
entries.sort_by(sort_entry);
|
||||
Ok(Json(entries).into_response())
|
||||
} else { // Redirect to file.
|
||||
Ok(Redirect::temporary(format!("/r/{target}").as_str()).into_response())
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn delete_entry(State(state): State<ServerState<'_>>,
|
||||
Path((owner, repo,path)): Path<(String, String, String)>,
|
||||
AuthBearer(token): AuthBearer) -> Result<StatusCode> {
|
||||
let repo = format!("{owner}/{repo}").to_lowercase(); // Repos are case-insensitive.
|
||||
let target = format!("{repo}/{path}");
|
||||
|
||||
let _ = authorize(&state.config.gitea_host, &token, &repo, "admin").await?;
|
||||
|
||||
let dir = path::Path::new(&state.config.upload_path).join(target);
|
||||
let metadata = match fs::metadata(&dir) {
|
||||
Ok(x) => x,
|
||||
Err(_) => return Ok(StatusCode::NOT_FOUND),
|
||||
};
|
||||
|
||||
if metadata.is_dir() {
|
||||
fs::remove_dir_all(&dir)?;
|
||||
} else if metadata.is_file() || metadata.is_symlink() {
|
||||
fs::remove_file(&dir)?;
|
||||
} else {
|
||||
return Err(ERR_IMPOSSIBLE_ENTRY)?;
|
||||
}
|
||||
|
||||
Ok(StatusCode::OK)
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct UploadFile {
|
||||
name: String,
|
||||
@ -442,11 +591,10 @@ struct UploadFile {
|
||||
|
||||
pub async fn upload(State(state): State<ServerState<'_>>,
|
||||
Path((owner, repo, feature)): Path<(String, String, String)>,
|
||||
headers: HeaderMap,
|
||||
AuthBearer(user_secret): AuthBearer,
|
||||
mut multipart: Multipart) -> Result<()> {
|
||||
let repo = format!("{owner}/{repo}").to_lowercase(); // Repos are case-insensitive.
|
||||
let feature = feature.to_lowercase();
|
||||
let user_secret = extract_token(headers)?; // Authorization should be the secret this time.
|
||||
|
||||
let (secret, features) = &state.sql.prepare(crate::query::UPLOAD_QUERY)?
|
||||
.iter()
|
||||
@ -461,10 +609,10 @@ pub async fn upload(State(state): State<ServerState<'_>>,
|
||||
.collect::<Result<Vec<(String, FeatureList)>>>()?[0];
|
||||
|
||||
(user_secret == *secret).then_some(0)
|
||||
.ok_or(APIError::Unauthorized{ repo: repo.to_owned() })?;
|
||||
.ok_or(ApiError::Unauthorized{ repo: repo.to_owned() })?;
|
||||
|
||||
features.contains(&feature).then_some(0)
|
||||
.ok_or(APIError::DisabledFeature{ feature: feature.clone() })?;
|
||||
.ok_or(ApiError::DisabledFeature{ feature: feature.clone() })?;
|
||||
|
||||
// Process multipart.
|
||||
let mut file = UploadFile{ folder: "".to_owned(), ..Default::default() };
|
||||
@ -483,7 +631,10 @@ pub async fn upload(State(state): State<ServerState<'_>>,
|
||||
}
|
||||
|
||||
let dir = path::Path::new(&state.config.upload_path).join(&repo).join(feature);
|
||||
fs::write(dir.join(&file.name), file.data)?;
|
||||
let filepath = dir.join(&file.name);
|
||||
fs::write(&filepath, file.data)?;
|
||||
fs::set_permissions(&filepath, Permissions::from_mode(FILE_PERM))?;
|
||||
|
||||
let tardir = dir.join(&file.folder);
|
||||
if file.folder != *"" {
|
||||
if fs::exists(&tardir)? {
|
||||
@ -491,15 +642,127 @@ pub async fn upload(State(state): State<ServerState<'_>>,
|
||||
}
|
||||
|
||||
fs::create_dir(&tardir)?;
|
||||
let output = Command::new("tar").args(["-xf", dir.join(&file.name).to_str().unwrap(), "-C", tardir.to_str().unwrap()])
|
||||
fs::set_permissions(&tardir, Permissions::from_mode(FOLDER_PERM))?;
|
||||
let tardir_s = tardir.to_str().unwrap();
|
||||
let output = Command::new("tar").args(["-xf", filepath.to_str().unwrap(), "-C", tardir_s])
|
||||
.output()?;
|
||||
|
||||
// If there was an error, remove everything because the operation was unsucessful.
|
||||
if !output.status.success() {
|
||||
fs::remove_dir_all(&tardir)?;
|
||||
fs::remove_file(dir.join(file.name))?;
|
||||
return Err(APIError::Other{ msg: "Failed to complete untar".to_owned() })
|
||||
fs::remove_file(&filepath)?;
|
||||
return Err(ERR_UNTAR)?
|
||||
}
|
||||
|
||||
// Run chmod to set extracted folder permissions.
|
||||
Command::new("find").args([tardir_s, "-type", "f", "-exec", "chmod", "0640", "{}", "+"])
|
||||
.output()?;
|
||||
Command::new("find").args([tardir_s, "-type", "d", "-exec", "chmod", "0750", "{}", "+"])
|
||||
.output()?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn status_to_404(request: axum::extract::Request, next: Next) -> Response {
|
||||
let mut response = next.run(request).await;
|
||||
*response.status_mut() = StatusCode::NOT_FOUND;
|
||||
response
|
||||
}
|
||||
|
||||
pub async fn get_not_found(request: axum::extract::Request) -> Response {
|
||||
let resp = ServeFile::new("static/404.html").try_call(request).await.unwrap();
|
||||
let mut resp = resp.map(Body::new);
|
||||
*resp.status_mut() = StatusCode::NOT_FOUND;
|
||||
resp
|
||||
}
|
||||
|
||||
pub async fn auth_file_routing(State(state): State<ServerState<'_>>,
|
||||
Path((owner, path)): Path<(String, String)>,
|
||||
headers: HeaderMap,
|
||||
jar: CookieJar,
|
||||
mut request: axum::extract::Request,
|
||||
next: Next) -> Result<Response> {
|
||||
*request.uri_mut() = format!("/{owner}/{path}").parse::<Uri>().unwrap();
|
||||
let target = format!("{owner}/{path}");
|
||||
|
||||
let mut path_s: Vec<_> = path.split("/").collect();
|
||||
let repo = path_s.remove(0);
|
||||
let repo = format!("{owner}/{repo}").to_lowercase(); // Repos are case-insensitive.
|
||||
let no_delete = path_s.is_empty() || path_s[0].is_empty(); // We must be on the feature-selection page.
|
||||
|
||||
// If we find an authorization, it is probably a redirect from /api/files
|
||||
let token = if let Some(x) = headers.get(AUTHORIZATION) {
|
||||
x.to_str()
|
||||
.map_err(|_| ApiError::Tokenless)?
|
||||
.to_owned()
|
||||
} else { // Otherwise, this is regular browsing, if no cookie token, user needs to login.
|
||||
match jar.get("token") {
|
||||
Some(x) => x.value_trimmed().to_owned(),
|
||||
None => return Ok(Redirect::temporary("/").into_response()),
|
||||
}
|
||||
};
|
||||
|
||||
// We use a timed hash map to temporarily store tokens. This is to prevent
|
||||
// flooding Gitea with authorization requests for each small HTTP request
|
||||
// when viewing inner sites. (Ex: loading assets for docs/)
|
||||
let is_admin = match state.token_map.get(&token) {
|
||||
Some(x) => x, // If it returns, then continue.
|
||||
None => {
|
||||
// Check if user can view repo.
|
||||
match authorize(&state.config.gitea_host, &token, &repo, "pull").await {
|
||||
Ok(json) => {
|
||||
let perm = json.get("permissions").unwrap().get("admin").unwrap().as_bool().unwrap();
|
||||
// Store this in the timed hash map for 5 minutes.
|
||||
state.token_map.insert(token, perm, Duration::new(300, 0));
|
||||
perm
|
||||
},
|
||||
Err(e) => {
|
||||
let e = match e {
|
||||
ApiError::RequestError(x) => x,
|
||||
_ => return Err(e) // Some other error, return it.
|
||||
};
|
||||
|
||||
// If the error is unauthorized, try refreshing the token.
|
||||
// If it's not found, return not found.
|
||||
// Otherwise, it's unknown and we send back to "/".
|
||||
let refresh = format!("/refresh?target=/r/{target}");
|
||||
return match e.status() {
|
||||
Some(StatusCode::UNAUTHORIZED) => Ok(Redirect::temporary(&refresh).into_response()),
|
||||
Some(StatusCode::NOT_FOUND) => Ok(get_not_found(request).await),
|
||||
_ => Ok(Redirect::temporary("/").into_response()),
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
let dir = path::Path::new(&state.config.upload_path).join(&target);
|
||||
let mut dir = match fs::read_dir(&dir) {
|
||||
Ok(x) => {
|
||||
match fs::exists(dir.join("index.html"))? {
|
||||
true => {
|
||||
if !path.ends_with('/') {
|
||||
// Need to make sure for a website given to ServeDir, this needs to end in a /, othewrise
|
||||
// ServerDir will issue a redirect with a {uri}/, but this is at the wrong location,
|
||||
// since this does not contain the /r/ prefix.
|
||||
*request.uri_mut() = format!("/{owner}/{path}/").parse::<Uri>().unwrap();
|
||||
}
|
||||
return Ok(next.run(request).await); // index.html exists, so pass to ServeDir.
|
||||
}
|
||||
false => x.peekable(), // We need to get the directories.
|
||||
}
|
||||
},
|
||||
Err(_) => return Ok(next.run(request).await), // Not a directory or file or not found, so pass to ServeDir.
|
||||
};
|
||||
|
||||
if dir.peek().is_none() {
|
||||
return Ok(get_not_found(request).await);
|
||||
}
|
||||
|
||||
// At this point, we should send an page with the list of files.
|
||||
let mut entries = dir.map(entry_from_dir_entry).collect::<Result<Vec<Entry>>>()?;
|
||||
entries.sort_by(sort_entry);
|
||||
|
||||
Ok(Html(crate::compile::content_folder(&state.config, target, &entries, is_admin, no_delete)?).into_response())
|
||||
}
|
||||
|
||||
151
src/compile.rs
151
src/compile.rs
@ -14,73 +14,64 @@ struct WebStates<'a> {
|
||||
confirm_on: &'a str,
|
||||
}
|
||||
|
||||
static STATES: WebStates = WebStates {
|
||||
auth: "auth",
|
||||
error: "error",
|
||||
loading: "loading",
|
||||
repo_selected: "repoSelected",
|
||||
repo_created: "repoCreated",
|
||||
button_on: "enabled",
|
||||
feature_on: "fEnabled",
|
||||
confirm_on: "enabled",
|
||||
};
|
||||
|
||||
#[derive(Template)]
|
||||
#[template(
|
||||
ext = "html",
|
||||
path = "html/contentDefault.html",
|
||||
whitespace = "suppress"
|
||||
)]
|
||||
struct Default<'a> {
|
||||
config: crate::ServerConfig,
|
||||
struct ContentDefault<'a> {
|
||||
config: &'a crate::ServerConfig,
|
||||
features: Vec<String>,
|
||||
content_text: String,
|
||||
force_msg: bool,
|
||||
state: WebStates<'a>,
|
||||
}
|
||||
|
||||
#[derive(Template)]
|
||||
#[template(ext = "html", path = "html/base.html", whitespace = "suppress")]
|
||||
struct NotFound {
|
||||
config: crate::ServerConfig,
|
||||
#[template(ext = "html", path = "html/notFound.html", whitespace = "suppress")]
|
||||
struct NotFound<'a> {
|
||||
config: &'a crate::ServerConfig,
|
||||
content_text: String,
|
||||
}
|
||||
|
||||
#[derive(Template)]
|
||||
#[template(path = "index.css", escape = "txt", whitespace = "suppress")]
|
||||
struct Css<'a> {
|
||||
force_msg: bool,
|
||||
state: WebStates<'a>,
|
||||
}
|
||||
|
||||
pub fn make_templates(config: &crate::ServerConfig) -> Result<()> {
|
||||
let states = WebStates {
|
||||
auth: "auth",
|
||||
error: "error",
|
||||
loading: "loading",
|
||||
repo_selected: "repoSelected",
|
||||
repo_created: "repoCreated",
|
||||
button_on: "enabled",
|
||||
feature_on: "fEnabled",
|
||||
confirm_on: "enabled",
|
||||
};
|
||||
#[derive(Template)]
|
||||
#[template(ext = "html", path = "html/refresh.html", whitespace = "suppress")]
|
||||
struct Refresh<'a> {
|
||||
config: &'a crate::ServerConfig,
|
||||
content_text: String,
|
||||
force_msg: bool,
|
||||
state: WebStates<'a>,
|
||||
}
|
||||
|
||||
let default = Default {
|
||||
config: config.clone(),
|
||||
features: crate::api::RepoFeature::iter()
|
||||
.map(|f| f.to_string())
|
||||
.collect(),
|
||||
content_text: "awaiting authorization...".to_owned(),
|
||||
state: states,
|
||||
}
|
||||
.render()
|
||||
.context("Unable to process 'Default' template")?;
|
||||
|
||||
fs::write("static/index.html", default).context("Could not write to static/index.html")?;
|
||||
|
||||
let not_found = NotFound {
|
||||
config: config.clone(),
|
||||
content_text: "404: not found...".to_owned(),
|
||||
}
|
||||
.render()
|
||||
.context("Unable to process 'NotFound' template.")?;
|
||||
|
||||
fs::write("static/404.html", not_found).context("Could not write to static/404.html")?;
|
||||
|
||||
let css = Css { state: states }
|
||||
.render()
|
||||
.context("Unable to process 'CSS' template.")?;
|
||||
|
||||
fs::write("static/index.css", css).context("Could not write to static/index.css")?;
|
||||
|
||||
Ok(())
|
||||
#[derive(Template)]
|
||||
#[template(
|
||||
ext = "html",
|
||||
path = "html/contentFolder.html",
|
||||
whitespace = "suppress"
|
||||
)]
|
||||
struct ContentFolder<'a> {
|
||||
config: &'a crate::ServerConfig,
|
||||
content_text: String,
|
||||
force_msg: bool,
|
||||
state: WebStates<'a>,
|
||||
target: String,
|
||||
entries: &'a Vec<crate::api::Entry>,
|
||||
is_admin: bool,
|
||||
no_delete: bool,
|
||||
}
|
||||
|
||||
fn copy_dir_all(src: impl AsRef<Path>, dst: impl AsRef<Path>) -> io::Result<()> {
|
||||
@ -98,7 +89,65 @@ fn copy_dir_all(src: impl AsRef<Path>, dst: impl AsRef<Path>) -> io::Result<()>
|
||||
}
|
||||
|
||||
pub fn copy_assets() -> Result<()> {
|
||||
fs::create_dir("static").context("Could not create directory 'static'.")?;
|
||||
fs::create_dir_all("static").context("Could not create directory 'static'.")?;
|
||||
copy_dir_all("assets", "static").context("Could not copy 'assets' into 'static'.")?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn content_folder(
|
||||
config: &crate::ServerConfig,
|
||||
target: String,
|
||||
entries: &Vec<crate::api::Entry>,
|
||||
is_admin: bool,
|
||||
no_delete: bool,
|
||||
) -> crate::api::Result<String> {
|
||||
Ok(ContentFolder {
|
||||
config,
|
||||
content_text: "awaiting authorization...".to_owned(),
|
||||
force_msg: false,
|
||||
state: STATES,
|
||||
target,
|
||||
entries,
|
||||
is_admin,
|
||||
no_delete,
|
||||
}
|
||||
.render()
|
||||
.map_err(|_| crate::api::ERR_CONTENT_FOLDER_RENDER)?)
|
||||
}
|
||||
|
||||
pub fn make_templates(config: &crate::ServerConfig) -> Result<()> {
|
||||
let default = ContentDefault {
|
||||
config,
|
||||
features: crate::api::RepoFeature::iter()
|
||||
.map(|f| f.to_string())
|
||||
.collect(),
|
||||
force_msg: false,
|
||||
content_text: "awaiting authorization...".to_owned(),
|
||||
state: STATES,
|
||||
}
|
||||
.render()
|
||||
.context("Unable to process 'Default' template")?;
|
||||
fs::write("static/index.html", default).context("Could not write to static/index.html")?;
|
||||
|
||||
let not_found = NotFound {
|
||||
config,
|
||||
content_text: "404: not found...".to_owned(),
|
||||
force_msg: true,
|
||||
state: STATES,
|
||||
}
|
||||
.render()
|
||||
.context("Unable to process 'NotFound' template.")?;
|
||||
fs::write("static/404.html", not_found).context("Could not write to static/404.html")?;
|
||||
|
||||
let refresh = Refresh {
|
||||
config,
|
||||
content_text: "refreshing...".to_owned(),
|
||||
force_msg: true,
|
||||
state: STATES,
|
||||
}
|
||||
.render()
|
||||
.context("Unable to process 'Refresh' template.")?;
|
||||
fs::write("static/refresh.html", refresh).context("Could not write to static/refresh.html")?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
41
src/main.rs
41
src/main.rs
@ -1,4 +1,5 @@
|
||||
use std::{
|
||||
convert::Infallible,
|
||||
fs,
|
||||
net::SocketAddr,
|
||||
};
|
||||
@ -6,10 +7,15 @@ use anyhow::{Context, Result};
|
||||
use axum::{
|
||||
body::Body,
|
||||
extract::{ConnectInfo, Request, DefaultBodyLimit},
|
||||
response::{IntoResponse, Response},
|
||||
routing::{get, post},
|
||||
middleware::{from_fn, from_fn_with_state},
|
||||
Router,
|
||||
http::StatusCode,
|
||||
};
|
||||
use tower::{ServiceBuilder, service_fn};
|
||||
use tower_http::{
|
||||
compression::CompressionLayer,
|
||||
services::{ServeDir, ServeFile},
|
||||
trace::TraceLayer,
|
||||
};
|
||||
@ -29,7 +35,7 @@ mod util;
|
||||
|
||||
type SQLConn = sqlite::ConnectionThreadSafe;
|
||||
|
||||
// type RepoMap = util::TimedHashMap<String, Vec<String>>;
|
||||
type TokenMap = util::TimedHashMap<String, bool>;
|
||||
|
||||
#[derive(Clone, Deserialize)]
|
||||
struct ServerConfig {
|
||||
@ -48,6 +54,7 @@ struct ServerConfig {
|
||||
struct ServerState<'a> {
|
||||
config: ServerConfig,
|
||||
sql: &'a SQLConn,
|
||||
token_map: &'a TokenMap,
|
||||
}
|
||||
|
||||
mod query {
|
||||
@ -91,7 +98,8 @@ async fn main() -> Result<()> {
|
||||
compile::make_templates(&config)?;
|
||||
|
||||
let sql: &'static sqlite::ConnectionThreadSafe = Box::leak(Box::new(sql));
|
||||
let server_state = ServerState { config, sql };
|
||||
let token_map: &'static TokenMap = Box::leak(Box::new(TokenMap::new()));
|
||||
let server_state = ServerState { config, sql, token_map };
|
||||
|
||||
let env_filter = EnvFilter::try_from_default_env()
|
||||
.or_else(|_| EnvFilter::try_new("info"))?;
|
||||
@ -122,22 +130,47 @@ async fn main() -> Result<()> {
|
||||
"request",
|
||||
method = format!("{}", request.method()),
|
||||
version = format!("{:?}", request.version()),
|
||||
path = request.uri().to_string(),
|
||||
remote_ip = addr,
|
||||
)
|
||||
});
|
||||
|
||||
let auth_browse_service = ServiceBuilder::new()
|
||||
.layer(from_fn_with_state(server_state.clone(), api::auth_file_routing))
|
||||
.service(ServeDir::new(upload_path));
|
||||
|
||||
let not_found_only = ServiceBuilder::new()
|
||||
.layer(from_fn(api::status_to_404))
|
||||
.service(service_fn(async |_| -> core::result::Result<Response, Infallible> {
|
||||
Ok(StatusCode::NOT_FOUND.into_response())
|
||||
}));
|
||||
|
||||
let not_found_file = ServiceBuilder::new()
|
||||
.layer(from_fn(api::status_to_404))
|
||||
.service(ServeFile::new("static/404.html"));
|
||||
|
||||
let compression_layer = CompressionLayer::new()
|
||||
.gzip(true)
|
||||
.br(true);
|
||||
|
||||
// Build routes
|
||||
let api_routes = Router::new()
|
||||
.route("/token", post(api::token).patch(api::refresh_token))
|
||||
.route("/repo/{owner}/{repo}", get(api::get_repo).post(api::create_repo).patch(api::patch_repo).delete(api::delete_repo))
|
||||
.route("/upload/{owner}/{repo}/{feature}", post(api::upload).layer(DefaultBodyLimit::max(usize::pow(1024, 3))))
|
||||
.route("/files/{owner}/{repo}/{*path}", get(api::get_entry).delete(api::delete_entry))
|
||||
.fallback_service(not_found_only)
|
||||
.with_state(server_state);
|
||||
|
||||
let app = Router::new()
|
||||
.nest("/api", api_routes)
|
||||
.route_service("/", ServeFile::new("static/index.html"))
|
||||
.nest_service("/assets", ServeDir::new("static"))
|
||||
.fallback_service(ServeDir::new(upload_path))
|
||||
.route_service("/", ServeFile::new("static/index.html"))
|
||||
.route_service("/favicon.ico", ServeFile::new("static/favicon.ico")) // For inner sites.
|
||||
.route_service("/refresh", ServeFile::new("static/refresh.html"))
|
||||
.route_service("/r/{owner}/{*path}", auth_browse_service)
|
||||
.fallback_service(not_found_file)
|
||||
.layer(compression_layer)
|
||||
.layer(trace);
|
||||
|
||||
// Run server.
|
||||
|
||||
125
src/util.rs
125
src/util.rs
@ -1,65 +1,74 @@
|
||||
// use std::{
|
||||
// collections::HashMap,
|
||||
// hash::Hash,
|
||||
// marker::Send,
|
||||
// sync::mpsc::{channel, Sender, TryRecvError},
|
||||
// sync::{Arc, Mutex},
|
||||
// thread,
|
||||
// time::{Duration, SystemTime},
|
||||
// };
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
hash::Hash,
|
||||
marker::Send,
|
||||
sync::mpsc::{channel, Sender, TryRecvError},
|
||||
sync::{Arc, Mutex},
|
||||
thread,
|
||||
time::{Duration, SystemTime},
|
||||
};
|
||||
|
||||
// pub struct TimedHashMap<K, V> {
|
||||
// map: Arc<Mutex<HashMap<K, (SystemTime, V)>>>,
|
||||
// duration: Arc<Mutex<Duration>>,
|
||||
// tx: Sender<i32>,
|
||||
// }
|
||||
pub struct TimedHashMap<K, V> {
|
||||
map: Arc<Mutex<HashMap<K, (SystemTime, V)>>>,
|
||||
duration: Arc<Mutex<Duration>>,
|
||||
_tx: Sender<i32>, // Needs to stay alive for the duration of the struct.
|
||||
}
|
||||
|
||||
// // Insert and cleanup send message to a main thread
|
||||
// // the main looping thread which reads messages to know its next action
|
||||
// Insert and cleanup send message to a main thread
|
||||
// the main looping thread which reads messages to know its next action
|
||||
|
||||
// impl<K, V> TimedHashMap<K, V>
|
||||
// where
|
||||
// K: std::cmp::Eq + Hash + Send + 'static,
|
||||
// V: Send + 'static,
|
||||
// {
|
||||
// pub fn new() -> Self {
|
||||
// let (tx, rx) = channel();
|
||||
// let thp = Self {
|
||||
// map: Arc::new(Mutex::new(HashMap::<K, (SystemTime, V)>::new())),
|
||||
// duration: Arc::new(Mutex::new(Duration::new(60, 0))),
|
||||
// tx,
|
||||
// };
|
||||
impl<K, V> TimedHashMap<K, V>
|
||||
where
|
||||
K: std::cmp::Eq + Hash + Send + 'static,
|
||||
V: Send + Clone + 'static,
|
||||
{
|
||||
pub fn new() -> Self {
|
||||
let (tx, rx) = channel();
|
||||
let thp = Self {
|
||||
map: Arc::new(Mutex::new(HashMap::<K, (SystemTime, V)>::new())),
|
||||
duration: Arc::new(Mutex::new(Duration::new(30, 0))),
|
||||
_tx: tx,
|
||||
};
|
||||
|
||||
// let map2 = Arc::clone(&thp.map);
|
||||
// let dur2 = Arc::clone(&thp.duration);
|
||||
// // Cleanup thread.
|
||||
// thread::spawn(move || loop {
|
||||
// thread::sleep(*dur2.lock().unwrap());
|
||||
// map2.lock().unwrap().retain(|_, v| v.0 < SystemTime::now());
|
||||
// match rx.try_recv() {
|
||||
// // Thread should get killed when tx drops.
|
||||
// Ok(_) | Err(TryRecvError::Disconnected) => break,
|
||||
// Err(TryRecvError::Empty) => {}
|
||||
// }
|
||||
// });
|
||||
let map2 = Arc::clone(&thp.map);
|
||||
let dur2 = Arc::clone(&thp.duration);
|
||||
// Cleanup thread.
|
||||
thread::spawn(move || loop {
|
||||
thread::sleep(*dur2.lock().unwrap());
|
||||
map2.lock().unwrap().retain(|_, v| v.0 < SystemTime::now());
|
||||
match rx.try_recv() {
|
||||
// Thread should get killed when tx drops.
|
||||
Ok(_) | Err(TryRecvError::Disconnected) => break,
|
||||
Err(TryRecvError::Empty) => {}
|
||||
}
|
||||
});
|
||||
|
||||
// thp
|
||||
// }
|
||||
thp
|
||||
}
|
||||
|
||||
// pub fn insert(&mut self, key: K, value: V, time: Duration) -> Option<V> {
|
||||
// let map2 = Arc::clone(&self.map);
|
||||
// match map2
|
||||
// .lock()
|
||||
// .unwrap()
|
||||
// .insert(key, (SystemTime::now() + time, value))
|
||||
// {
|
||||
// Some(x) => Some(x.1),
|
||||
// None => None,
|
||||
// }
|
||||
// }
|
||||
pub fn insert(&self, key: K, value: V, time: Duration) -> Option<V> {
|
||||
let map2 = Arc::clone(&self.map);
|
||||
match map2
|
||||
.lock()
|
||||
.unwrap()
|
||||
.insert(key, (SystemTime::now() + time, value))
|
||||
{
|
||||
Some(x) => Some(x.1),
|
||||
None => None,
|
||||
}
|
||||
}
|
||||
|
||||
// pub fn set_interval(&mut self, duration: Duration) {
|
||||
// let dur2 = Arc::clone(&self.duration);
|
||||
// *dur2.lock().unwrap() = duration
|
||||
// }
|
||||
// }
|
||||
pub fn get(&self, key: &K) -> Option<V> {
|
||||
let map2 = Arc::clone(&self.map);
|
||||
map2.lock()
|
||||
.unwrap()
|
||||
.get(key)
|
||||
.as_ref()
|
||||
.map(|x| (x.1).clone())
|
||||
}
|
||||
|
||||
// pub fn set_interval(&mut self, duration: Duration) {
|
||||
// let dur2 = Arc::clone(&self.duration);
|
||||
// *dur2.lock().unwrap() = duration
|
||||
// }
|
||||
}
|
||||
|
||||
172
templates/css/contentDefault.css
Normal file
172
templates/css/contentDefault.css
Normal file
@ -0,0 +1,172 @@
|
||||
{% include "css/mod/base.css" %}
|
||||
|
||||
#packageLabelContent {
|
||||
> div:nth-of-type(1) {
|
||||
padding: 7% 7% 0 7%;
|
||||
color: var(--c-text);
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin: 0;
|
||||
line-height: 100%;
|
||||
}
|
||||
|
||||
input {
|
||||
background: none;
|
||||
color: var(--c-text);
|
||||
border: 1px solid var(--c-lines);
|
||||
padding: 2%;
|
||||
margin: 2% 0 2% 0;
|
||||
font-size: 150%;
|
||||
font-family: 'Courier';
|
||||
|
||||
&.{{ state.error }} { outline: 2px solid red !important; }
|
||||
&:focus { outline: none; }
|
||||
}
|
||||
|
||||
input[type=submit] {
|
||||
cursor: pointer;
|
||||
transition: background-color 0.15s ease-in-out;
|
||||
|
||||
&:hover { background-color: var(--c-login-hover); }
|
||||
|
||||
/* Button off. */
|
||||
color: var(--c-null-text);
|
||||
border-style: dashed;
|
||||
pointer-events: none;
|
||||
|
||||
&.{{ state.button_on }} {
|
||||
color: var(--c-text);
|
||||
border-style: solid;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
&.{{ state.loading }} {
|
||||
border: 1px solid transparent;
|
||||
background: linear-gradient(var(--c-label), var(--c-label)) padding-box, conic-gradient(
|
||||
from var(--angle),
|
||||
var(--c-lines) 0% 30%,
|
||||
var(--c-loading) 30% 50%,
|
||||
var(--c-lines) 50% 80%,
|
||||
var(--c-loading) 80% 100%
|
||||
) border-box;
|
||||
animation: 1.5s rotate linear infinite;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
|
||||
#repoSelect {
|
||||
border-bottom: 2px solid var(--c-lines);
|
||||
|
||||
#repoSelectInput { margin-bottom: 0; }
|
||||
#repoSelectButton { float: right; }
|
||||
|
||||
#repoSelectError {
|
||||
line-height: 100%;
|
||||
height: 3rem;
|
||||
margin: 2% 0 2% 0;
|
||||
}
|
||||
}
|
||||
|
||||
#repoInfo {
|
||||
margin-top: 2%;
|
||||
transition: opacity 0.15s ease-in-out;
|
||||
|
||||
> h1:first-child { margin-top: 4%;}
|
||||
|
||||
#repoActions {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
#repoLinkInput::selection { background-color: transparent; }
|
||||
#repoLinkButton { float: right; }
|
||||
|
||||
#repoFeatures {
|
||||
margin-top: 2%;
|
||||
transition: opacity 0.15s ease-in-out;
|
||||
|
||||
#featureBox {
|
||||
display: flex;
|
||||
flex-flow: row wrap;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
|
||||
> .feature {
|
||||
margin-right: 2%;
|
||||
font-weight: 800;
|
||||
pointer-events: auto;
|
||||
|
||||
/* Feature off */
|
||||
color: var(--c-null-text);
|
||||
border-style: dashed;
|
||||
|
||||
&.{{ state.feature_on }} {
|
||||
color: var(--c-text);
|
||||
border-style: solid;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#confirmOverlay {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
|
||||
position: absolute;
|
||||
left: 0;
|
||||
z-index: 10;
|
||||
|
||||
transition: opacity 0.15s ease-in-out;
|
||||
|
||||
/* Confirm off */
|
||||
pointer-events: none;
|
||||
top: 100%;
|
||||
opacity: 0;
|
||||
|
||||
&.{{ state.confirm_on }} {
|
||||
pointer-events: auto;
|
||||
top: 0;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
> div {
|
||||
margin: 30% auto 0 auto;
|
||||
|
||||
width: 80rem;
|
||||
height: 30rem;
|
||||
|
||||
outline-width: 5rem;
|
||||
outline-style: solid;
|
||||
outline-color: var(--c-label);
|
||||
|
||||
border-radius: 12px;
|
||||
background-color: var(--c-label);
|
||||
border: 0.5rem solid var(--c-lines);
|
||||
|
||||
transition: background-color 0.15s ease-in-out,
|
||||
border 0.15s ease-in-out,
|
||||
outline-color 0.15s ease-in-out;
|
||||
|
||||
> div {
|
||||
padding: 7%;
|
||||
|
||||
> input[type=submit] { float: right; }
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/* Repo selected and created states. */
|
||||
#repoInfo, #repoFeatures {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
#repoInfo.{{ state.repo_selected }}, #repoFeatures.{{ state.repo_created }} {
|
||||
opacity: 1
|
||||
}
|
||||
}
|
||||
135
templates/css/contentFolder.css
Normal file
135
templates/css/contentFolder.css
Normal file
@ -0,0 +1,135 @@
|
||||
{% include "css/mod/base.css" %}
|
||||
|
||||
#packageLabelContent {
|
||||
> div {
|
||||
padding: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
svg {
|
||||
padding: 0 5% 0 5%;
|
||||
> path { pointer-events: none; }
|
||||
}
|
||||
|
||||
table {
|
||||
border-spacing: 0;
|
||||
font-size: 150%;
|
||||
width: 100%;
|
||||
|
||||
th {
|
||||
height: 6rem;
|
||||
|
||||
> svg {
|
||||
width: 2.5rem;
|
||||
padding-right: 6%;
|
||||
cursor: pointer;
|
||||
fill: var(--c-back-button);
|
||||
transition: opacity 0.15s ease-in-out;
|
||||
|
||||
&:hover { opacity: 0.6 }
|
||||
}
|
||||
|
||||
&:first-child {
|
||||
text-align: left;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
tr {
|
||||
height: 6rem;
|
||||
transition: background-color 0.15s ease-in-out;
|
||||
|
||||
&:first-child { background-color: var(--c-table-header); }
|
||||
/* Excluding header */
|
||||
&:nth-child(2n+3) { background-color: var(--c-table-odd); }
|
||||
/* Excluding header */
|
||||
&:nth-child(n+1):hover { background-color: var(--c-table-hover); }
|
||||
}
|
||||
|
||||
td {
|
||||
height: 6rem;
|
||||
|
||||
&:nth-of-type(1) {
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
> svg { width: 3rem; }
|
||||
> a { color: var(--c-text); }
|
||||
}
|
||||
|
||||
&:nth-of-type(2) {
|
||||
width: 20%;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
&:nth-of-type(3) {
|
||||
width: 16%;
|
||||
text-align: center;
|
||||
|
||||
> svg {
|
||||
width: 2.3rem;
|
||||
margin-top: 5%;
|
||||
cursor: pointer;
|
||||
transition: fill 0.15s ease-in-out;
|
||||
|
||||
&:hover { fill: #ff3f3f; }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#confirmOverlay {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
|
||||
position: absolute;
|
||||
left: 0;
|
||||
z-index: 10;
|
||||
|
||||
transition: opacity 0.15s ease-in-out;
|
||||
|
||||
/* Confirm off */
|
||||
pointer-events: none;
|
||||
top: 100%;
|
||||
opacity: 0;
|
||||
|
||||
&.{{ state.confirm_on }} {
|
||||
pointer-events: auto;
|
||||
top: 0;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
> div {
|
||||
margin: 40% auto 0 auto;
|
||||
|
||||
width: 80rem;
|
||||
height: 20rem;
|
||||
|
||||
outline-width: 5rem;
|
||||
outline-style: solid;
|
||||
outline-color: var(--c-label);
|
||||
|
||||
border-radius: 12px;
|
||||
background-color: var(--c-label);
|
||||
border: 0.5rem solid var(--c-lines);
|
||||
|
||||
transition: background-color 0.15s ease-in-out,
|
||||
border 0.15s ease-in-out,
|
||||
outline-color 0.15s ease-in-out;
|
||||
|
||||
> div {
|
||||
padding: 7%;
|
||||
|
||||
> input[type=submit]:first-child {
|
||||
float: right;
|
||||
margin-bottom: 5%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -12,6 +12,10 @@
|
||||
--cl-lines: black;
|
||||
--cl-login-hover: rgba(255, 255, 255, 0.8);
|
||||
--cl-null-text: #9b9b9b;
|
||||
--cl-table-header: #d6f0a7;
|
||||
--cl-table-odd: #dadada;
|
||||
--cl-table-hover: #c6dc9e;
|
||||
--cl-back-button: rgba(0, 0, 0, 0.3);
|
||||
|
||||
/* Dark Colors */
|
||||
--cd-bg: #111;
|
||||
@ -24,6 +28,10 @@
|
||||
--cd-lines: #4d4d4d;
|
||||
--cd-login-hover: rgba(0, 0, 0, 0.5);
|
||||
--cd-null-text: #9b9b9b;
|
||||
--cd-table-header: #314a48;
|
||||
--cd-table-odd: #2d2d2d;
|
||||
--cd-table-hover: #344543;
|
||||
--cd-back-button: rgba(255, 255, 255, 0.3);
|
||||
|
||||
--c-loading: #d6d6d6;
|
||||
|
||||
@ -38,6 +46,10 @@
|
||||
--c-lines: var(--cl-lines);
|
||||
--c-login-hover: var(--cl-login-hover);
|
||||
--c-null-text: var(--cl-null-text);
|
||||
--c-table-header: var(--cl-table-header);
|
||||
--c-table-odd: var(--cl-table-odd);
|
||||
--c-table-hover: var(--cl-table-hover);
|
||||
--c-back-button: var(--cl-back-button);
|
||||
|
||||
--p-theme-switch-0: 0;
|
||||
--p-theme-switch-1: 1;
|
||||
@ -54,6 +66,10 @@
|
||||
--c-lines: var(--cd-lines);
|
||||
--c-login-hover: var(--cd-login-hover);
|
||||
--c-null-text: var(--cd-null-text);
|
||||
--c-table-header: var(--cd-table-header);
|
||||
--c-table-odd: var(--cd-table-odd);
|
||||
--c-table-hover: var(--cd-table-hover);
|
||||
--c-back-button: var(--cd-back-button);
|
||||
|
||||
--p-theme-switch-0: 1;
|
||||
--p-theme-switch-1: 0;
|
||||
@ -74,6 +90,10 @@
|
||||
--c-lines: var(--cl-lines);
|
||||
--c-login-hover: var(--cl-login-hover);
|
||||
--c-null-text: var(--cl-null-text);
|
||||
--c-table-header: var(--cl-table-header);
|
||||
--c-table-odd: var(--cl-table-odd);
|
||||
--c-table-hover: var(--cl-table-hover);
|
||||
--c-back-button: var(--cl-back-button);
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: light) {
|
||||
@ -87,6 +107,10 @@
|
||||
--c-lines: var(--cd-lines);
|
||||
--c-login-hover: var(--cd-login-hover);
|
||||
--c-null-text: var(--cd-null-text);
|
||||
--c-table-header: var(--cd-table-header);
|
||||
--c-table-odd: var(--cd-table-odd);
|
||||
--c-table-hover: var(--cd-table-hover);
|
||||
--c-back-button: var(--cd-back-button);
|
||||
}
|
||||
}
|
||||
|
||||
@ -139,76 +163,70 @@ body {
|
||||
--w: 6deg;
|
||||
--spokes: 10;
|
||||
|
||||
input {
|
||||
> input {
|
||||
width: 100%; height: 100%;
|
||||
position: absolute;
|
||||
opacity: 0;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover ~ span:nth-child(2) {
|
||||
filter: brightness(110%);
|
||||
}
|
||||
|
||||
&:checked ~ span {
|
||||
&:nth-of-type(1) { background-color: var(--night-color); }
|
||||
&:nth-of-type(2) { opacity: var(--p-theme-switch-0); }
|
||||
&:nth-of-type(3) { opacity: var(--p-theme-switch-1); }
|
||||
}
|
||||
}
|
||||
|
||||
span {
|
||||
> span {
|
||||
transition: opacity 0.15s ease-in-out;
|
||||
position: absolute;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
span:nth-child(2) {
|
||||
width: 100%; height: 100%;
|
||||
top: 0; left: 0;
|
||||
background-color: var(--day-color);
|
||||
filter: brightness(100%);
|
||||
transition: background-color 0.15s ease-in-out,
|
||||
filter 0.15s ease-in-out;
|
||||
}
|
||||
&:nth-of-type(1) {
|
||||
width: 100%; height: 100%;
|
||||
top: 0; left: 0;
|
||||
background-color: var(--day-color);
|
||||
filter: brightness(100%);
|
||||
transition: background-color 0.15s ease-in-out,
|
||||
filter 0.15s ease-in-out;
|
||||
}
|
||||
|
||||
span:nth-child(3) {
|
||||
width: 4rem; height: 4rem;
|
||||
position: absolute;
|
||||
top: 3rem; left: 3rem;
|
||||
background-color: var(--sun-color);
|
||||
border-radius: 50%;
|
||||
opacity: var(--p-theme-switch-1);
|
||||
}
|
||||
&:nth-of-type(2) {
|
||||
width: 4rem; height: 4rem;
|
||||
position: absolute;
|
||||
top: 3rem; left: 3rem;
|
||||
background-color: var(--sun-color);
|
||||
border-radius: 50%;
|
||||
opacity: var(--p-theme-switch-1);
|
||||
|
||||
span:nth-child(3)::before {
|
||||
content: "";
|
||||
width: 7rem; height: 7rem;
|
||||
position: absolute;
|
||||
top: -1.5rem; left: -1.5rem;
|
||||
border-radius: inherit;
|
||||
background-image: repeating-conic-gradient(
|
||||
from calc(-1*var(--w)/2), var(--sun-color) 0 calc(var(--w) - 2deg),
|
||||
transparent calc(var(--w)) calc(360deg/var(--spokes) - 2deg),
|
||||
var(--sun-color) calc(360deg/var(--spokes))
|
||||
);
|
||||
}
|
||||
&::before {
|
||||
content: "";
|
||||
width: 7rem; height: 7rem;
|
||||
position: absolute;
|
||||
top: -1.5rem; left: -1.5rem;
|
||||
border-radius: inherit;
|
||||
background-image: repeating-conic-gradient(
|
||||
from calc(-1*var(--w)/2), var(--sun-color) 0 calc(var(--w) - 2deg),
|
||||
transparent calc(var(--w)) calc(360deg/var(--spokes) - 2deg),
|
||||
var(--sun-color) calc(360deg/var(--spokes))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
span:nth-child(4) {
|
||||
width: 6rem; height: 6rem;
|
||||
top: 1.75rem; left: 1.75rem;
|
||||
background-color: var(--moon-color);
|
||||
border-radius: 50%;
|
||||
opacity: var(--p-theme-switch-0);
|
||||
mask-image: radial-gradient(circle, rgba(0,0,0,0) 33%, black 34%);
|
||||
mask-position: -3rem -3rem;
|
||||
mask-repeat: no-repeat;
|
||||
mask-size: 166.66% 166.66%;
|
||||
}
|
||||
|
||||
input:hover ~ span:nth-child(2) {
|
||||
filter: brightness(110%);
|
||||
}
|
||||
|
||||
input:checked ~ span:nth-child(2) {
|
||||
background-color: var(--night-color);
|
||||
}
|
||||
|
||||
input:checked ~ span:nth-child(3) {
|
||||
opacity: var(--p-theme-switch-0);
|
||||
}
|
||||
|
||||
input:checked ~ span:nth-child(4) {
|
||||
opacity: var(--p-theme-switch-1);
|
||||
&:nth-of-type(3) {
|
||||
width: 6rem; height: 6rem;
|
||||
top: 1.75rem; left: 1.75rem;
|
||||
background-color: var(--moon-color);
|
||||
border-radius: 50%;
|
||||
opacity: var(--p-theme-switch-0);
|
||||
mask-image: radial-gradient(circle, rgba(0,0,0,0) 33%, black 34%);
|
||||
mask-position: -3rem -3rem;
|
||||
mask-repeat: no-repeat;
|
||||
mask-size: 166.66% 166.66%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -230,29 +248,29 @@ body {
|
||||
z-index: 1;
|
||||
user-select: none;
|
||||
|
||||
img {
|
||||
> img {
|
||||
display: block;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
:nth-child(1) {
|
||||
width: 30rem;
|
||||
top: 6rem; left: 263rem;
|
||||
}
|
||||
&:nth-of-type(1) {
|
||||
width: 30rem;
|
||||
top: 6rem; left: 263rem;
|
||||
}
|
||||
|
||||
:nth-child(2) {
|
||||
width: 30rem;
|
||||
top: 6rem; left: 228rem;
|
||||
}
|
||||
&:nth-of-type(2) {
|
||||
width: 30rem;
|
||||
top: 6rem; left: 228rem;
|
||||
}
|
||||
|
||||
:nth-child(3) {
|
||||
width: 35rem;
|
||||
top: 162rem; left: 257rem;
|
||||
}
|
||||
&:nth-of-type(3) {
|
||||
width: 35rem;
|
||||
top: 162rem; left: 257rem;
|
||||
}
|
||||
|
||||
:nth-child(4) {
|
||||
width: 25rem;
|
||||
top: 165rem; left: 10rem;
|
||||
&:nth-of-type(4) {
|
||||
width: 25rem;
|
||||
top: 165rem; left: 10rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -262,7 +280,7 @@ body {
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
|
||||
text {
|
||||
> text {
|
||||
font: 1.5px var(--font-family);
|
||||
fill: rgba(255, 255, 255, 0.4);
|
||||
user-select: none;
|
||||
@ -300,48 +318,46 @@ body {
|
||||
grid-template-columns: 1fr 3fr;
|
||||
border-bottom: 0.5rem solid var(--c-lines);
|
||||
|
||||
> div:nth-child(1) {
|
||||
> div:nth-of-type(1) {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
border-right: 0.5rem solid var(--c-lines);
|
||||
|
||||
svg {
|
||||
> svg {
|
||||
width: 80%; height: 80%;
|
||||
}
|
||||
}
|
||||
|
||||
> div:nth-child(2) {
|
||||
> div:nth-of-type(2) {
|
||||
padding: 3%;
|
||||
display: grid;
|
||||
grid-template-rows: 1fr 1fr;
|
||||
|
||||
> div:nth-child(1) {
|
||||
> div:nth-of-type(1) {
|
||||
display: block;
|
||||
color: var(--c-dec-text);
|
||||
user-select: none;
|
||||
|
||||
p {
|
||||
> p {
|
||||
margin: 0;
|
||||
font-size: 150%;
|
||||
}
|
||||
|
||||
p:nth-child(1) {
|
||||
display: inline-block;
|
||||
}
|
||||
&:nth-of-type(1) { display: inline-block; }
|
||||
&:nth-of-type(2) {
|
||||
display: inline-block;
|
||||
float: right;
|
||||
}
|
||||
|
||||
p:nth-child(2) {
|
||||
display: inline-block;
|
||||
float: right;
|
||||
}
|
||||
}
|
||||
|
||||
> div:nth-child(2) {
|
||||
> div:nth-of-type(2) {
|
||||
width: 80%;
|
||||
position: relative;
|
||||
|
||||
svg {
|
||||
> svg {
|
||||
position: absolute;
|
||||
left: 0; top: 0;
|
||||
width: 35rem; height: 7rem;
|
||||
@ -379,7 +395,7 @@ body {
|
||||
border-bottom: 0.5rem solid var(--c-lines);
|
||||
user-select: none;
|
||||
|
||||
h1 {
|
||||
> h1 {
|
||||
margin: 1%;
|
||||
font-size: 430%;
|
||||
}
|
||||
@ -396,9 +412,9 @@ body {
|
||||
}
|
||||
|
||||
@property --angle {
|
||||
syntax: "<angle>";
|
||||
initial-value: 0deg;
|
||||
inherits: false;
|
||||
syntax: "<angle>";
|
||||
initial-value: 0deg;
|
||||
inherits: false;
|
||||
}
|
||||
|
||||
#packageLabelContent {
|
||||
@ -417,213 +433,18 @@ body {
|
||||
animation: 1.5s infinite normal cursorWait;
|
||||
content: "hello";
|
||||
}
|
||||
|
||||
> div:nth-child(2) {
|
||||
padding: 7% 7% 0 7%;
|
||||
color: var(--c-text);
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin: 0;
|
||||
line-height: 100%;
|
||||
}
|
||||
|
||||
input {
|
||||
background: none;
|
||||
color: var(--c-text);
|
||||
border: 1px solid var(--c-lines);
|
||||
padding: 2%;
|
||||
margin: 2% 0 2% 0;
|
||||
font-size: 150%;
|
||||
font-family: 'Courier';
|
||||
}
|
||||
|
||||
input.{{ state.error }} {
|
||||
outline: 2px solid red !important;
|
||||
}
|
||||
|
||||
input[type=submit] {
|
||||
cursor: pointer;
|
||||
transition: background-color 0.15s ease-in-out;
|
||||
}
|
||||
|
||||
/* Button enabled/disabled. */
|
||||
input[type=submit] {
|
||||
color: var(--c-null-text);
|
||||
border-style: dashed;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
input[type=submit].{{ state.button_on }} {
|
||||
color: var(--c-text);
|
||||
border-style: solid;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
input[type=submit]:hover {
|
||||
background-color: var(--c-login-hover);
|
||||
}
|
||||
|
||||
input:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
input[type=submit].{{ state.loading }} {
|
||||
border: 1px solid transparent;
|
||||
background: linear-gradient(var(--c-label), var(--c-label)) padding-box, conic-gradient(
|
||||
from var(--angle),
|
||||
var(--c-lines) 0% 30%,
|
||||
var(--c-loading) 30% 50%,
|
||||
var(--c-lines) 50% 80%,
|
||||
var(--c-loading) 80% 100%
|
||||
) border-box;
|
||||
animation: 1.5s rotate linear infinite;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
#repoSelect {
|
||||
border-bottom: 2px solid var(--c-lines);
|
||||
|
||||
#repoSelectInput {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
#repoSelectButton {
|
||||
float: right;
|
||||
}
|
||||
|
||||
#repoSelectError {
|
||||
line-height: 100%;
|
||||
height: 3rem;
|
||||
margin: 2% 0 2% 0;
|
||||
}
|
||||
}
|
||||
|
||||
#repoInfo {
|
||||
margin-top: 2%;
|
||||
transition: opacity 0.15s ease-in-out;
|
||||
|
||||
> h1:first-child {
|
||||
margin-top: 4%;
|
||||
}
|
||||
|
||||
#repoActions {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
#repoLinkInput::selection {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
#repoLinkButton {
|
||||
float: right;
|
||||
}
|
||||
|
||||
#repoFeatures {
|
||||
margin-top: 2%;
|
||||
transition: opacity 0.15s ease-in-out;
|
||||
|
||||
#featureBox {
|
||||
display: flex;
|
||||
flex-flow: row wrap;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
|
||||
.feature {
|
||||
margin-right: 2%;
|
||||
font-weight: 800;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
/* Feature states. */
|
||||
.feature {
|
||||
color: var(--c-null-text);
|
||||
border-style: dashed;
|
||||
}
|
||||
|
||||
.feature.{{ state.feature_on }} {
|
||||
color: var(--c-text);
|
||||
border-style: solid;
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#confirmOverlay {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
|
||||
position: absolute;
|
||||
left: 0;
|
||||
z-index: 10;
|
||||
|
||||
transition: opacity 0.15s ease-in-out;
|
||||
|
||||
> div {
|
||||
margin: 30% auto 0 auto;
|
||||
|
||||
width: 80rem;
|
||||
height: 30rem;
|
||||
|
||||
outline-width: 5rem;
|
||||
outline-style: solid;
|
||||
outline-color: var(--c-label);
|
||||
|
||||
border-radius: 12px;
|
||||
background-color: var(--c-label);
|
||||
border: 0.5rem solid var(--c-lines);
|
||||
|
||||
transition: background-color 0.15s ease-in-out,
|
||||
border 0.15s ease-in-out,
|
||||
outline-color 0.15s ease-in-out;
|
||||
|
||||
> div {
|
||||
padding: 7%;
|
||||
|
||||
input[type=submit] {
|
||||
float: right;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#confirmOverlay {
|
||||
pointer-events: none;
|
||||
top: 100%;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
#confirmOverlay.{{ state.confirm_on }} {
|
||||
pointer-events: auto;
|
||||
top: 0;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* Repo selected and created states. */
|
||||
#repoInfo, #repoFeatures {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
#repoInfo.{{ state.repo_selected }}, #repoFeatures.{{ state.repo_created }} {
|
||||
opacity: 1
|
||||
}
|
||||
}
|
||||
|
||||
#packageLabelBot {
|
||||
display: grid;
|
||||
grid-template-rows: 6fr 1fr;
|
||||
|
||||
svg {
|
||||
> svg {
|
||||
margin: 3% auto 0 auto;
|
||||
width: 80%; height: 85%;
|
||||
}
|
||||
|
||||
a {
|
||||
> a {
|
||||
margin: auto;
|
||||
margin-bottom: 1%;
|
||||
font-size: 2rem;
|
||||
@ -631,60 +452,41 @@ body {
|
||||
text-decoration: none;
|
||||
transition: filter 0.15s ease-in-out;
|
||||
filter: brightness(100%);
|
||||
}
|
||||
|
||||
a:hover {
|
||||
filter: brightness(120%);
|
||||
&:hover { filter: brightness(120%); }
|
||||
}
|
||||
}
|
||||
|
||||
body { /* Default no authentication */
|
||||
#packageLabelTitle h1::before {
|
||||
content: "PRIORITY PACKAGING"
|
||||
}
|
||||
#packageLabelTitle > h1::before { content: "PRIORITY PACKAGING" }
|
||||
|
||||
#packageLabelContent > div {
|
||||
display: none;
|
||||
}
|
||||
#packageLabelContent > div { display: none; }
|
||||
|
||||
#login {
|
||||
background: var(--c-label);
|
||||
color: var(--c-text);
|
||||
}
|
||||
|
||||
#login::before {
|
||||
content: "LOGIN";
|
||||
}
|
||||
|
||||
#login:hover {
|
||||
background-color: var(--c-login-hover);
|
||||
&::before { content: "LOGIN"; }
|
||||
&:hover { background-color: var(--c-login-hover); }
|
||||
}
|
||||
}
|
||||
|
||||
body.{{ state.auth }} {
|
||||
#packageLabelTitle h1::before {
|
||||
content: "PACKAGE INFO"
|
||||
}
|
||||
#packageLabelTitle h1::before { content: "PACKAGE INFO" }
|
||||
|
||||
#packageLabelContent > div {
|
||||
display: block;
|
||||
}
|
||||
|
||||
#packageLabelContent > h1 {
|
||||
display: none;
|
||||
#packageLabelContent {
|
||||
> div { display: block; }
|
||||
> h1 { display: none; }
|
||||
}
|
||||
|
||||
#login {
|
||||
color: transparent;
|
||||
background:transparent;
|
||||
}
|
||||
|
||||
#login::before {
|
||||
content: "LOGOUT";
|
||||
}
|
||||
|
||||
#login:hover {
|
||||
background: var(--c-label);
|
||||
color: var(--c-text);
|
||||
&::before { content: "LOGOUT"; }
|
||||
&:hover {
|
||||
background: var(--c-label);
|
||||
color: var(--c-text);
|
||||
}
|
||||
}
|
||||
}
|
||||
1
templates/css/notFound.css
Normal file
1
templates/css/notFound.css
Normal file
@ -0,0 +1 @@
|
||||
{% include "css/mod/base.css" %}
|
||||
1
templates/css/refresh.css
Normal file
1
templates/css/refresh.css
Normal file
@ -0,0 +1 @@
|
||||
{% include "css/mod/base.css" %}
|
||||
@ -1,4 +1,8 @@
|
||||
{% extends "base.html" %}
|
||||
{% extends "mod/base.html" %}
|
||||
|
||||
{% block style %}
|
||||
{% include "css/contentDefault.css" %}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div>
|
||||
@ -22,7 +26,7 @@
|
||||
<h1>FEATURES:</h1>
|
||||
<div id="featureBox">
|
||||
{% for feature in features %}
|
||||
<input class="feature" type="submit" data-tag="{{ feature | lower }}" value="{{ feature }}">
|
||||
<input class="feature" type="submit" data-tag="{{ feature | lower }}" value="{{ feature }}">
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
@ -42,6 +46,5 @@
|
||||
{% endblock %}
|
||||
|
||||
{% block script %}
|
||||
{% include "js/token.js" %}
|
||||
{% include "js/default.js" %}
|
||||
{% include "js/contentDefault.js" %}
|
||||
{% endblock %}
|
||||
|
||||
61
templates/html/contentFolder.html
Normal file
61
templates/html/contentFolder.html
Normal file
@ -0,0 +1,61 @@
|
||||
{% extends "mod/base.html" %}
|
||||
|
||||
{% block title %}Pack - /r/{{ target }}{% endblock %}
|
||||
|
||||
{% block style %}
|
||||
{% include "css/contentFolder.css" %}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div>
|
||||
<table>
|
||||
<tr>
|
||||
<th>
|
||||
<svg id="back" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 512"><!--!Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2025 Fonticons, Inc.--><path d="M41.4 233.4c-12.5 12.5-12.5 32.8 0 45.3l160 160c12.5 12.5 32.8 12.5 45.3 0s12.5-32.8 0-45.3L109.3 256 246.6 118.6c12.5-12.5 12.5-32.8 0-45.3s-32.8-12.5-45.3 0l-160 160z"/></svg>
|
||||
Name
|
||||
</th>
|
||||
<th>Size</th>
|
||||
{% if is_admin && !no_delete %}
|
||||
<th></th>
|
||||
{% endif %}
|
||||
</tr>
|
||||
{% for entry in entries %}
|
||||
<tr>
|
||||
<td>
|
||||
{% match entry.icon %}
|
||||
{% when "folder" %} <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!--!Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2025 Fonticons, Inc.--><path d="M64 480H448c35.3 0 64-28.7 64-64V160c0-35.3-28.7-64-64-64H288c-10.1 0-19.6-4.7-25.6-12.8L243.2 57.6C231.1 41.5 212.1 32 192 32H64C28.7 32 0 60.7 0 96V416c0 35.3 28.7 64 64 64z"/></svg>
|
||||
{% when "text" %} <svg style="height:3rem;" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 384 512"><!--!Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2025 Fonticons, Inc.--><path d="M64 0C28.7 0 0 28.7 0 64L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-288-128 0c-17.7 0-32-14.3-32-32L224 0 64 0zM256 0l0 128 128 0L256 0zM112 256l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16zm0 64l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16zm0 64l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z"/></svg>
|
||||
{% when "archive" %} <svg style="height:3rem;" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 384 512"><!--!Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2025 Fonticons, Inc.--><path d="M64 0C28.7 0 0 28.7 0 64L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-288-128 0c-17.7 0-32-14.3-32-32L224 0 64 0zM256 0l0 128 128 0L256 0zM96 48c0-8.8 7.2-16 16-16l32 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-32 0c-8.8 0-16-7.2-16-16zm0 64c0-8.8 7.2-16 16-16l32 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-32 0c-8.8 0-16-7.2-16-16zm0 64c0-8.8 7.2-16 16-16l32 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-32 0c-8.8 0-16-7.2-16-16zm-6.3 71.8c3.7-14 16.4-23.8 30.9-23.8l14.8 0c14.5 0 27.2 9.7 30.9 23.8l23.5 88.2c1.4 5.4 2.1 10.9 2.1 16.4c0 35.2-28.8 63.7-64 63.7s-64-28.5-64-63.7c0-5.5 .7-11.1 2.1-16.4l23.5-88.2zM112 336c-8.8 0-16 7.2-16 16s7.2 16 16 16l32 0c8.8 0 16-7.2 16-16s-7.2-16-16-16l-32 0z"/></svg>
|
||||
{% when "binary" %} <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><!--!Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2025 Fonticons, Inc.--><path d="M0 96C0 60.7 28.7 32 64 32l320 0c35.3 0 64 28.7 64 64l0 320c0 35.3-28.7 64-64 64L64 480c-35.3 0-64-28.7-64-64L0 96zm144 4c-24.3 0-44 19.7-44 44l0 48c0 24.3 19.7 44 44 44l32 0c24.3 0 44-19.7 44-44l0-48c0-24.3-19.7-44-44-44l-32 0zm-4 44c0-2.2 1.8-4 4-4l32 0c2.2 0 4 1.8 4 4l0 48c0 2.2-1.8 4-4 4l-32 0c-2.2 0-4-1.8-4-4l0-48zm140-44c-11 0-20 9-20 20c0 9.7 6.9 17.7 16 19.6l0 76.4c0 11 9 20 20 20s20-9 20-20l0-96c0-11-9-20-20-20l-16 0zM132 296c0 9.7 6.9 17.7 16 19.6l0 76.4c0 11 9 20 20 20s20-9 20-20l0-96c0-11-9-20-20-20l-16 0c-11 0-20 9-20 20zm96 24l0 48c0 24.3 19.7 44 44 44l32 0c24.3 0 44-19.7 44-44l0-48c0-24.3-19.7-44-44-44l-32 0c-24.3 0-44 19.7-44 44zm44-4l32 0c2.2 0 4 1.8 4 4l0 48c0 2.2-1.8 4-4 4l-32 0c-2.2 0-4-1.8-4-4l0-48c0-2.2 1.8-4 4-4z"/></svg>
|
||||
{% else %} <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 384 512"><!--!Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2025 Fonticons, Inc.--><path d="M0 64C0 28.7 28.7 0 64 0L224 0l0 128c0 17.7 14.3 32 32 32l128 0 0 288c0 35.3-28.7 64-64 64L64 512c-35.3 0-64-28.7-64-64L0 64zm384 64l-128 0L256 0 384 128z"/></svg>
|
||||
{% endmatch %}
|
||||
{% match entry.icon %}
|
||||
{% when "folder" %} <a href="./{{ entry.name_uri }}/">{{ entry.name }}</a>
|
||||
{% else %} <a href="./{{ entry.name_uri }}">{{ entry.name }}</a>
|
||||
{% endmatch %}
|
||||
</td>
|
||||
<td>{{ entry.size }}</td>
|
||||
{% if is_admin && !no_delete %}
|
||||
<td>
|
||||
<svg class="delete" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><!--!Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2025 Fonticons, Inc.--><path d="M135.2 17.7L128 32 32 32C14.3 32 0 46.3 0 64S14.3 96 32 96l384 0c17.7 0 32-14.3 32-32s-14.3-32-32-32l-96 0-7.2-14.3C307.4 6.8 296.3 0 284.2 0L163.8 0c-12.1 0-23.2 6.8-28.6 17.7zM416 128L32 128 53.2 467c1.6 25.3 22.6 45 47.9 45l245.8 0c25.3 0 46.3-19.7 47.9-45L416 128z"/></svg>
|
||||
</td>
|
||||
{% endif %}
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
</div>
|
||||
<div id="confirmOverlay">
|
||||
<div>
|
||||
<div>
|
||||
<h1>CONFIRMATION:</h1>
|
||||
<p>This will (irreversibly) delete the file! Are you sure?</p>
|
||||
<input class="enabled" id="confirmButtonYes" type="submit" value="Confirm">
|
||||
<input class="enabled" id="confirmButtonNo" type="submit" value="Cancel">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block script %}
|
||||
{% include "js/contentFolder.js" %}
|
||||
{% endblock %}
|
||||
@ -2,11 +2,12 @@
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="chrome=1">
|
||||
<title>Pack</title>
|
||||
<link rel="icon" href="assets/favicon.ico?v=2">
|
||||
<link rel="stylesheet" href="assets/index.css">
|
||||
<title>{% block title %}Pack{% endblock %}</title>
|
||||
<link rel="icon" href="/assets/favicon.ico?v=2">
|
||||
<!-- <link rel="stylesheet" href="/assets/index.css"> -->
|
||||
<link href="https://fonts.googleapis.com/css?family=Quicksand:300,400" rel="stylesheet">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no">
|
||||
<style>{% block style %}{% endblock %}</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="themeSwitchContainer">
|
||||
@ -14,29 +15,30 @@
|
||||
<span></span><span></span><span></span>
|
||||
</div>
|
||||
<div id="package">
|
||||
<img src="assets/fragile.svg">
|
||||
<img src="assets/keep_dry.svg">
|
||||
<img src="assets/recycle.svg">
|
||||
<img src="assets/qr_code.svg">
|
||||
<img src="/assets/fragile.svg">
|
||||
<img src="/assets/keep_dry.svg">
|
||||
<img src="/assets/recycle.svg">
|
||||
<img src="/assets/qr_code.svg">
|
||||
</div>
|
||||
{% include "svgTape.svg" %}
|
||||
{% include "html/mod/svgTape.svg" %}
|
||||
<div id="packageLabel">
|
||||
<div id="packageLabelTop">{% include "labelTop.html" %}</div>
|
||||
<div id="packageLabelTop">{% include "html/mod/labelTop.html" %}</div>
|
||||
<div id="packageLabelTitle">
|
||||
<h1><b></b></h1>
|
||||
</div>
|
||||
<div id="packageLabelContent">
|
||||
{% if force_msg %}
|
||||
<h1 style="display: block !important;">{{ content_text | upper }}</h1>
|
||||
{% else %}
|
||||
<h1>{{ content_text | upper }}</h1>
|
||||
{% endif %}
|
||||
{% block content %}{% endblock %}
|
||||
</div>
|
||||
<div id="packageLabelBot">
|
||||
{% include "labelBot.svg" %}
|
||||
{% include "html/mod/labelBot.svg" %}
|
||||
<a href="/">{{ config.domain }}</a>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
<script>
|
||||
{% include "js/base.js" %}
|
||||
{% block script %}{% endblock %}
|
||||
</script>
|
||||
<script>{% block script %}{% endblock %}</script>
|
||||
</html>
|
||||
|
Before Width: | Height: | Size: 3.2 KiB After Width: | Height: | Size: 3.2 KiB |
@ -1,14 +1,14 @@
|
||||
<div>
|
||||
<svg width="48" height="48" viewBox="0 0 12.7 12.7" version="1.1">
|
||||
<defs id="defs2" />
|
||||
<path d="m 12.323869,4.1992062 -6.2301843,2.5959093 -1e-7,0.095169 6.3500004,-2.6458336 z" />
|
||||
<path d="M 12.443685,4.2444508 V 4.3735617 L 9.2686845,4.925839 V 4.7967281 Z" />
|
||||
<path d="M 6.0936845,1.6003079 12.443177,4.2450847 9.2686845,4.796728 2.9191918,2.1519511 Z" />
|
||||
<path d="m 9.2686845,3.8880996 0.062387,0.1201924 -3.237387,3.0585598 2e-7,-0.17699 z" />
|
||||
<path d="M 9.2686845,4.7967281 V 4.925839 L 8.6378103,4.6632589 8.7123025,4.564976 Z" />
|
||||
<rect width="6.8791666" height="5.8977885" x="-13.480659" y="9.4293194" transform="matrix(-0.92307692,0.3846154,0,1,0,0)" />
|
||||
<rect width="6.8791666" height="5.8977885" x="-0.27767482" y="4.3512492" transform="matrix(0.92307692,0.3846154,0,1,0,0)" />
|
||||
<path d="M 2.9186843,1.2431113 9.2686845,3.8880996 6.0936849,6.8898618 -0.2563155,4.2448735 Z" />
|
||||
<defs id="defs2" />
|
||||
<path d="m 12.323869,4.1992062 -6.2301843,2.5959093 -1e-7,0.095169 6.3500004,-2.6458336 z" />
|
||||
<path d="M 12.443685,4.2444508 V 4.3735617 L 9.2686845,4.925839 V 4.7967281 Z" />
|
||||
<path d="M 6.0936845,1.6003079 12.443177,4.2450847 9.2686845,4.796728 2.9191918,2.1519511 Z" />
|
||||
<path d="m 9.2686845,3.8880996 0.062387,0.1201924 -3.237387,3.0585598 2e-7,-0.17699 z" />
|
||||
<path d="M 9.2686845,4.7967281 V 4.925839 L 8.6378103,4.6632589 8.7123025,4.564976 Z" />
|
||||
<rect width="6.8791666" height="5.8977885" x="-13.480659" y="9.4293194" transform="matrix(-0.92307692,0.3846154,0,1,0,0)" />
|
||||
<rect width="6.8791666" height="5.8977885" x="-0.27767482" y="4.3512492" transform="matrix(0.92307692,0.3846154,0,1,0,0)" />
|
||||
<path d="M 2.9186843,1.2431113 9.2686845,3.8880996 6.0936849,6.8898618 -0.2563155,4.2448735 Z" />
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
|
Before Width: | Height: | Size: 5.4 KiB After Width: | Height: | Size: 5.4 KiB |
11
templates/html/notFound.html
Normal file
11
templates/html/notFound.html
Normal file
@ -0,0 +1,11 @@
|
||||
{% extends "mod/base.html" %}
|
||||
|
||||
{% block title %}Pack - 404{% endblock %}
|
||||
|
||||
{% block style %}
|
||||
{% include "css/notFound.css" %}
|
||||
{% endblock %}
|
||||
|
||||
{% block script %}
|
||||
{% include "js/notFound.js" %}
|
||||
{% endblock %}
|
||||
9
templates/html/refresh.html
Normal file
9
templates/html/refresh.html
Normal file
@ -0,0 +1,9 @@
|
||||
{% extends "mod/base.html" %}
|
||||
|
||||
{% block style %}
|
||||
{% include "css/refresh.css" %}
|
||||
{% endblock %}
|
||||
|
||||
{% block script %}
|
||||
{% include "js/refresh.js" %}
|
||||
{% endblock %}
|
||||
@ -1,50 +1,9 @@
|
||||
const STATES = { // Either the non-d
|
||||
auth: "{{ state.auth }}",
|
||||
error: "{{ state.error }}",
|
||||
loading: "{{ state.loading }}",
|
||||
repoSelected: "{{ state.repo_selected }}",
|
||||
repoCreated: "{{ state.repo_created }}",
|
||||
buttonOn: "{{ state.button_on }}",
|
||||
featureOn: "{{ state.feature_on }}",
|
||||
confirmOn: "{{ state.confirm_on }}",
|
||||
}
|
||||
{% include "js/mod/base.js" %}
|
||||
{% include "js/mod/token.js" %}
|
||||
{% include "js/mod/middleware.js" %}
|
||||
{% include "js/mod/confirm.js" %}
|
||||
|
||||
var G_REPO_VALUE = "",
|
||||
G_CONFIRM_VALUE = "",
|
||||
G_CONFIRM = -1;
|
||||
|
||||
// UTILITY //
|
||||
|
||||
// Parse a query string into an object
|
||||
function parseQueryString(string) {
|
||||
if(string == "") { return {}; }
|
||||
var segments = string.split("&").map(s => s.split("=") );
|
||||
var queryString = {};
|
||||
segments.forEach(s => queryString[s[0]] = s[1]);
|
||||
return queryString;
|
||||
}
|
||||
|
||||
// BUTTON ONCLICK MIDDLEWARE //
|
||||
|
||||
// Refresh token if necessary.
|
||||
function tryAuth(f) {
|
||||
return async (e) => {
|
||||
e.preventDefault();
|
||||
if (!G_AUTH) return;
|
||||
await tryRefresh();
|
||||
await f(e);
|
||||
}
|
||||
}
|
||||
|
||||
// Adds loading classes for UI while awaiting.
|
||||
function doLoading(f) {
|
||||
return async (e) => {
|
||||
if (e.target.hasClass("loading")) return;
|
||||
e.target.addClass(STATES["loading"]);
|
||||
await f(e);
|
||||
e.target.delClass(STATES["loading"]);
|
||||
}
|
||||
}
|
||||
var G_REPO_VALUE = "";
|
||||
|
||||
// BUTTON ONCLICK HELPERS //
|
||||
|
||||
@ -64,26 +23,6 @@ function setRepoActions(s) {
|
||||
get("#repoFeatures").setClass(STATES["repoCreated"], s);
|
||||
}
|
||||
|
||||
async function getConfirm() {
|
||||
get("#confirmOverlay").addClass(STATES["confirmOn"]);
|
||||
return new Promise((resolve, reject) => {
|
||||
const check = function() {
|
||||
if (G_CONFIRM == -1) {
|
||||
setTimeout(check, 100);
|
||||
} else {
|
||||
get("#confirmOverlay").delClass(STATES["confirmOn"]);
|
||||
resolve(G_CONFIRM);
|
||||
G_CONFIRM = -1;
|
||||
}
|
||||
}
|
||||
check();
|
||||
});
|
||||
}
|
||||
|
||||
document.on("keydown", (e) => {
|
||||
if (e.keyCode == 27) get("#confirmOverlay").delClass(STATES["confirmOn"]);
|
||||
});
|
||||
|
||||
// BUTTON ONCLICK FUNCTIONS //
|
||||
|
||||
get("#repoSelectInput").on("keydown", (e) => {
|
||||
@ -125,7 +64,7 @@ async function selectRepo(e) {
|
||||
});
|
||||
}
|
||||
|
||||
get("#repoLinkInput").value = CONFIG["redirect_uri"] + repo;
|
||||
get("#repoLinkInput").value = CONFIG["redirect_uri"] + "r/" + repo;
|
||||
get("#repoInfo").addClass(STATES["repoSelected"]);
|
||||
|
||||
})
|
||||
42
templates/js/contentFolder.js
Normal file
42
templates/js/contentFolder.js
Normal file
@ -0,0 +1,42 @@
|
||||
{% include "js/mod/base.js" %}
|
||||
{% include "js/mod/token.js" %}
|
||||
{% include "js/mod/middleware.js" %}
|
||||
{% include "js/mod/confirm.js" %}
|
||||
|
||||
get("#confirmButtonNo").on("click", (e) => G_CONFIRM = false );
|
||||
get("#confirmButtonYes").on("click", (e) => G_CONFIRM = true );
|
||||
|
||||
get(".delete").forEach((el) => el.on("click", tryAuth(deleteEntry)));
|
||||
async function deleteEntry(e) {
|
||||
let target = e.target.parentElement.parentElement.children[0].children[1];
|
||||
target = target.href.replace("/r", "/api/files");
|
||||
if (!(await getConfirm())) return;
|
||||
await jfetch(target, "DELETE")
|
||||
.then((json) => {
|
||||
e.target.parentElement.parentElement.remove();
|
||||
})
|
||||
.catch(getErr((err) => {
|
||||
console.log(err);
|
||||
alert("An unexpected error occurred! Check console for more details.");
|
||||
}));
|
||||
}
|
||||
|
||||
get("#back").on("click", tryAuth(goBack));
|
||||
async function goBack(e) {
|
||||
let path = window.location.pathname.split("/");
|
||||
if (path.length === 5) { // Looking at repo features, so back goes to home.
|
||||
window.location = "/";
|
||||
} else {
|
||||
window.location = path.slice(0, -2).join("/");
|
||||
}
|
||||
}
|
||||
|
||||
function init() {
|
||||
let url = window.location.toString();
|
||||
if (url[url.length-1] !== "/") {
|
||||
window.history.pushState({}, "", url + "/");
|
||||
}
|
||||
}
|
||||
|
||||
init();
|
||||
|
||||
@ -6,7 +6,16 @@ const CONFIG = {
|
||||
requested_scopes: "read:repository,write:repository"
|
||||
};
|
||||
|
||||
var G_AUTH = window.localStorage.getItem("token") !== null;
|
||||
const STATES = {
|
||||
auth: "{{ state.auth }}",
|
||||
error: "{{ state.error }}",
|
||||
loading: "{{ state.loading }}",
|
||||
repoSelected: "{{ state.repo_selected }}",
|
||||
repoCreated: "{{ state.repo_created }}",
|
||||
buttonOn: "{{ state.button_on }}",
|
||||
featureOn: "{{ state.feature_on }}",
|
||||
confirmOn: "{{ state.confirm_on }}",
|
||||
}
|
||||
|
||||
document.on = document.addEventListener;
|
||||
function sugar(obj) {
|
||||
@ -44,10 +53,20 @@ function getErr(f) {
|
||||
}
|
||||
}
|
||||
|
||||
// Parse a query string into an object
|
||||
function parseQueryString(string) {
|
||||
if(string == "") { return {}; }
|
||||
var segments = string.split("&").map(s => s.split("=") );
|
||||
var queryString = {};
|
||||
segments.forEach(s => queryString[s[0]] = s[1]);
|
||||
return queryString;
|
||||
}
|
||||
|
||||
get("#login").on("click", async (e) => {
|
||||
e.preventDefault();
|
||||
if (G_AUTH) {
|
||||
window.localStorage.clear();
|
||||
document.cookie = "";
|
||||
window.location = "/";
|
||||
} else {
|
||||
// Build the authorization URL
|
||||
@ -61,3 +80,6 @@ get("#login").on("click", async (e) => {
|
||||
window.location = url;
|
||||
}
|
||||
});
|
||||
|
||||
var G_AUTH = window.localStorage.getItem("token") !== null;
|
||||
get("body").setClass(STATES["auth"], G_AUTH);
|
||||
22
templates/js/mod/confirm.js
Normal file
22
templates/js/mod/confirm.js
Normal file
@ -0,0 +1,22 @@
|
||||
var G_CONFIRM_VALUE = "",
|
||||
G_CONFIRM = -1;
|
||||
|
||||
async function getConfirm() {
|
||||
get("#confirmOverlay").addClass(STATES["confirmOn"]);
|
||||
return new Promise((resolve, reject) => {
|
||||
const check = function() {
|
||||
if (G_CONFIRM == -1) {
|
||||
setTimeout(check, 100);
|
||||
} else {
|
||||
get("#confirmOverlay").delClass(STATES["confirmOn"]);
|
||||
resolve(G_CONFIRM);
|
||||
G_CONFIRM = -1;
|
||||
}
|
||||
}
|
||||
check();
|
||||
});
|
||||
}
|
||||
|
||||
document.on("keydown", (e) => {
|
||||
if (e.keyCode == 27) get("#confirmOverlay").delClass(STATES["confirmOn"]);
|
||||
});
|
||||
19
templates/js/mod/middleware.js
Normal file
19
templates/js/mod/middleware.js
Normal file
@ -0,0 +1,19 @@
|
||||
// Refresh token if necessary.
|
||||
function tryAuth(f) {
|
||||
return async (e) => {
|
||||
e.preventDefault();
|
||||
if (!G_AUTH) return;
|
||||
await tryRefresh();
|
||||
await f(e);
|
||||
}
|
||||
}
|
||||
|
||||
// Adds loading classes for UI while awaiting.
|
||||
function doLoading(f) {
|
||||
return async (e) => {
|
||||
if (e.target.hasClass("loading")) return;
|
||||
e.target.addClass(STATES["loading"]);
|
||||
await f(e);
|
||||
e.target.delClass(STATES["loading"]);
|
||||
}
|
||||
}
|
||||
@ -3,7 +3,7 @@ async function jfetch(url, method, obj={}) {
|
||||
let request = {
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": window.localStorage.getItem("token"),
|
||||
"Authorization": `Bearer ${window.localStorage.getItem("token")}`,
|
||||
},
|
||||
method: method,
|
||||
};
|
||||
@ -39,6 +39,7 @@ async function getToken() {
|
||||
window.localStorage.setItem("token", response.access_token);
|
||||
window.localStorage.setItem("token_expiry", (Date.now() + response.expires_in*1000).toString());
|
||||
window.localStorage.setItem("refresh_token", response.refresh_token);
|
||||
document.cookie = `token=${response.access_token}`;
|
||||
}
|
||||
|
||||
async function tryRefresh() {
|
||||
1
templates/js/notFound.js
Normal file
1
templates/js/notFound.js
Normal file
@ -0,0 +1 @@
|
||||
{% include "js/mod/base.js" %}
|
||||
11
templates/js/refresh.js
Normal file
11
templates/js/refresh.js
Normal file
@ -0,0 +1,11 @@
|
||||
{% include "js/mod/base.js" %}
|
||||
{% include "js/mod/token.js" %}
|
||||
|
||||
async function init() {
|
||||
if (!G_QUERY.target) window.location = "/";
|
||||
await tryRefresh();
|
||||
window.location = G_QUERY.target;
|
||||
}
|
||||
|
||||
var G_QUERY = parseQueryString(window.location.search.substring(1));
|
||||
init();
|
||||
Loading…
x
Reference in New Issue
Block a user