diff --git a/.dockerignore b/.dockerignore
new file mode 100644
index 000000000..796603f34
--- /dev/null
+++ b/.dockerignore
@@ -0,0 +1,2 @@
+target
+.env
\ No newline at end of file
diff --git a/.env b/.env
index 7c251a033..292f2d3e3 100644
--- a/.env
+++ b/.env
@@ -3,13 +3,14 @@ DEBUG=true
CDN_URL=cdn.modrinth.com
-MONGODB_ADDR=mongodb://toor:modrinthadmin@localhost:27017
+DATABASE_URL=postgresql://labrinth@localhost/labrinth
MEILISEARCH_ADDR=http://localhost:7700
BIND_ADDR=127.0.0.1:8000
MOCK_FILE_PATH=/tmp/modrinth
+BACKBLAZE_ENABLED=false
BACKBLAZE_KEY_ID=none
BACKBLAZE_KEY=none
-BACKBLAZE_BUCKET_ID=none
\ No newline at end of file
+BACKBLAZE_BUCKET_ID=none
diff --git a/.github/workflows/codequality.yml b/.github/workflows/codequality.yml
index af0e68a5c..10ac064c8 100644
--- a/.github/workflows/codequality.yml
+++ b/.github/workflows/codequality.yml
@@ -7,6 +7,7 @@ on:
env:
CARGO_TERM_COLOR: always
+ SQLX_OFFLINE: true
jobs:
lint:
@@ -30,3 +31,5 @@ jobs:
with:
token: ${{ secrets.GITHUB_TOKEN }}
args: --all-features
+ env:
+ SQLX_OFFLINE: true
diff --git a/.github/workflows/docker-compile-master.yml b/.github/workflows/docker-compile-master.yml
new file mode 100644
index 000000000..c942698cf
--- /dev/null
+++ b/.github/workflows/docker-compile-master.yml
@@ -0,0 +1,25 @@
+name: Docker image build
+
+on:
+ push:
+ branches:
+ - master
+env:
+ CARGO_TERM_COLOR: always
+ SQLX_OFFLINE: true
+
+jobs:
+ build:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v2
+ - name: Build and push Docker images
+ uses: docker/build-push-action@v1
+ with:
+ username: ${{ github.actor }}
+ password: ${{ secrets.GITHUB_TOKEN }}
+ registry: docker.pkg.github.com
+ repository: modrinth/labrinth/labrinth
+ tag_with_ref: false
+ tags: master
+ tag_with_sha: true
\ No newline at end of file
diff --git a/.github/workflows/docker-compile-tag.yml b/.github/workflows/docker-compile-tag.yml
new file mode 100644
index 000000000..a0088ef65
--- /dev/null
+++ b/.github/workflows/docker-compile-tag.yml
@@ -0,0 +1,26 @@
+name: Docker image build
+
+on:
+ push:
+ # Sequence of patterns matched against refs/tags
+ tags:
+ - 'v*' # Push events to matching v*, i.e. v1.0, v20.15.10
+env:
+ CARGO_TERM_COLOR: always
+ SQLX_OFFLINE: true
+
+jobs:
+ build:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v2
+ - name: Build and push Docker images
+ uses: docker/build-push-action@v1
+ with:
+ username: ${{ github.actor }}
+ password: ${{ secrets.GITHUB_TOKEN }}
+ registry: docker.pkg.github.com
+ repository: modrinth/labrinth/labrinth
+ tag_with_ref: true
+ tags: latest
+ tag_with_sha: true
\ No newline at end of file
diff --git a/.github/workflows/docker-compile.yml b/.github/workflows/docker-compile.yml
new file mode 100644
index 000000000..728ec34cd
--- /dev/null
+++ b/.github/workflows/docker-compile.yml
@@ -0,0 +1,25 @@
+name: Docker image build
+
+on:
+ push:
+ branches:
+ - !master
+ pull_request:
+env:
+ CARGO_TERM_COLOR: always
+ SQLX_OFFLINE: true
+
+jobs:
+ build:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v2
+ - name: Build and push Docker images
+ uses: docker/build-push-action@v1
+ with:
+ username: ${{ github.actor }}
+ password: ${{ secrets.GITHUB_TOKEN }}
+ registry: docker.pkg.github.com
+ repository: modrinth/labrinth/labrinth
+ tag_with_ref: true
+ tag_with_sha: true
\ No newline at end of file
diff --git a/.github/workflows/labelsync.yml b/.github/workflows/labelsync.yml
index 0df86cac8..cfe672d5a 100644
--- a/.github/workflows/labelsync.yml
+++ b/.github/workflows/labelsync.yml
@@ -5,7 +5,7 @@ on:
label:
push:
branches:
- - maser
+ - master
paths:
- '.github/labels.yml'
diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml
index 633d748e6..867f7cf70 100644
--- a/.github/workflows/rust.yml
+++ b/.github/workflows/rust.yml
@@ -7,6 +7,7 @@ on:
env:
CARGO_TERM_COLOR: always
+ SQLX_OFFLINE: true
jobs:
build:
@@ -25,7 +26,11 @@ jobs:
uses: actions-rs/toolchain@v1
with:
toolchain: stable
+ env:
+ SQLX_OFFLINE: true
- uses: actions-rs/cargo@v1
name: Build program
with:
command: build
+ env:
+ SQLX_OFFLINE: true
diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
index 007b10587..aa2e97608 100644
--- a/.github/workflows/tests.yml
+++ b/.github/workflows/tests.yml
@@ -2,20 +2,27 @@ name: Unit Tests
on:
push:
- branches: [ master ]
+ branches: [master]
pull_request:
+env:
+ CARGO_TERM_COLOR: always
+ SQLX_OFFLINE: true
+
jobs:
test:
name: ${{ matrix.os }}-rust-${{ matrix.rust }}
runs-on: ${{ matrix.os }}
+ continue-on-error: ${{ matrix.continue-on-error }}
strategy:
matrix:
os: [ubuntu-latest]
- rust:
- - stable
- - beta
- - nightly
+ rust: [beta, nightly]
+ continue-on-error: [true]
+ include:
+ - os: ubuntu-latest
+ - rust: stable
+ - continue-on-error: false
steps:
- uses: actions/checkout@v2
@@ -34,10 +41,13 @@ jobs:
- uses: actions-rs/cargo@v1
with:
command: build
+ env:
+ SQLX_OFFLINE: true
- uses: actions-rs/cargo@v1
with:
command: test
env:
BACKBLAZE_BUCKET_ID: ${{ secrets.BACKBLAZE_BUCKET_ID }}
BACKBLAZE_KEY: ${{ secrets.BACKBLAZE_KEY }}
- BACKBLAZE_KEY_ID: ${{ secrets.BACKBLAZE_KEY_ID }}
\ No newline at end of file
+ BACKBLAZE_KEY_ID: ${{ secrets.BACKBLAZE_KEY_ID }}
+ SQLX_OFFLINE: true
diff --git a/.gitignore b/.gitignore
index c011fbb0e..e129ca0d6 100644
--- a/.gitignore
+++ b/.gitignore
@@ -97,7 +97,10 @@ fabric.properties
# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries
# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html
-Cargo.lock
+# Cargo.lock
+# Since we're using git dependencies, Cargo.lock is required to stop random
+# errors when an upstream build fails. We're also making an executable, so we
+# shouldn't have it in the .gitignore
# These are backup files generated by rustfmt
**/*.rs.bk
diff --git a/.idea/dataSources.xml b/.idea/dataSources.xml
new file mode 100644
index 000000000..0c2adc54e
--- /dev/null
+++ b/.idea/dataSources.xml
@@ -0,0 +1,11 @@
+
+
+
+
+ postgresql
+ true
+ org.postgresql.Driver
+ jdbc:postgresql://localhost:5432/labrinth
+
+
+
\ No newline at end of file
diff --git a/.idea/deployment.xml b/.idea/deployment.xml
new file mode 100644
index 000000000..52206eb2c
--- /dev/null
+++ b/.idea/deployment.xml
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/sqldialects.xml b/.idea/sqldialects.xml
new file mode 100644
index 000000000..b13859f47
--- /dev/null
+++ b/.idea/sqldialects.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 811329230..741fad4bc 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -6,10 +6,15 @@ We reccomend using [Docker](https://www.docker.com/) for setting up your dev env
```sh
docker-compose up
```
-which will deploy a Meilisearch container on port 7700, a MongoDB instance on port 27017 and a MongoDB web UI on port 8081
+which will deploy a Meilisearch container on port 7700, a PostgreSQL container on port 5432 and a pgAdmin web UI on port 8070.
+When prompted to input a server password in pgAdmin, simply enter nothing into the password field.
-Alternatively, follow theese steps:
-1. Install and run a [MeiliSearch](https://docs.meilisearch.com/guides/introduction/quick_start_guide.html) instance
-2. Install [A local MongoDB server](https://www.mongodb.com/try/download/community)
-3. Run `mongod --dbpath path/to/db`
-4. Everything should be setup and you should be ready to contribute.
\ No newline at end of file
+You will have to set up the database now. To do so, install the sqlx cli:
+```sh
+cargo install --git https://github.com/launchbadge/sqlx sqlx-cli --no-default-features --features postgres
+```
+then, run the following commands to create the database and install schemas:
+```sh
+sqlx database create
+sqlx migrate run
+```
diff --git a/Cargo.lock b/Cargo.lock
new file mode 100644
index 000000000..be8499351
--- /dev/null
+++ b/Cargo.lock
@@ -0,0 +1,2653 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+[[package]]
+name = "actix-codec"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "09e55f0a5c2ca15795035d90c46bd0e73a5123b72f68f12596d6ba5282051380"
+dependencies = [
+ "bitflags",
+ "bytes",
+ "futures-core",
+ "futures-sink",
+ "log",
+ "tokio",
+ "tokio-util 0.2.0",
+]
+
+[[package]]
+name = "actix-connect"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c95cc9569221e9802bf4c377f6c18b90ef10227d787611decf79fd47d2a8e76c"
+dependencies = [
+ "actix-codec",
+ "actix-rt",
+ "actix-service",
+ "actix-utils",
+ "derive_more",
+ "either",
+ "futures",
+ "http",
+ "log",
+ "trust-dns-proto",
+ "trust-dns-resolver",
+]
+
+[[package]]
+name = "actix-files"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "193b22cb1f7b4ff12a4eb2415d6d19e47e44ea93e05930b30d05375ea29d3529"
+dependencies = [
+ "actix-http",
+ "actix-service",
+ "actix-web",
+ "bitflags",
+ "bytes",
+ "derive_more",
+ "futures-core",
+ "futures-util",
+ "log",
+ "mime",
+ "mime_guess",
+ "percent-encoding",
+ "v_htmlescape",
+]
+
+[[package]]
+name = "actix-http"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c16664cc4fdea8030837ad5a845eb231fb93fc3c5c171edfefb52fad92ce9019"
+dependencies = [
+ "actix-codec",
+ "actix-connect",
+ "actix-rt",
+ "actix-service",
+ "actix-threadpool",
+ "actix-utils",
+ "base64 0.11.0",
+ "bitflags",
+ "brotli2",
+ "bytes",
+ "chrono",
+ "copyless",
+ "derive_more",
+ "either",
+ "encoding_rs",
+ "failure",
+ "flate2",
+ "futures-channel",
+ "futures-core",
+ "futures-util",
+ "fxhash",
+ "h2",
+ "http",
+ "httparse",
+ "indexmap",
+ "language-tags",
+ "lazy_static",
+ "log",
+ "mime",
+ "percent-encoding",
+ "pin-project",
+ "rand",
+ "regex",
+ "serde",
+ "serde_json",
+ "serde_urlencoded",
+ "sha1",
+ "slab",
+ "time",
+]
+
+[[package]]
+name = "actix-macros"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a60f9ba7c4e6df97f3aacb14bb5c0cd7d98a49dcbaed0d7f292912ad9a6a3ed2"
+dependencies = [
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "actix-multipart"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4397935fca2a37a5353f94faa758fb176712806f605466b5a60373b204f0d836"
+dependencies = [
+ "actix-service",
+ "actix-utils",
+ "actix-web",
+ "bytes",
+ "derive_more",
+ "futures",
+ "httparse",
+ "log",
+ "mime",
+ "time",
+ "twoway",
+]
+
+[[package]]
+name = "actix-router"
+version = "0.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9d7a10ca4d94e8c8e7a87c5173aba1b97ba9a6563ca02b0e1cd23531093d3ec8"
+dependencies = [
+ "bytestring",
+ "http",
+ "log",
+ "regex",
+ "serde",
+]
+
+[[package]]
+name = "actix-rt"
+version = "1.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "143fcc2912e0d1de2bcf4e2f720d2a60c28652ab4179685a1ee159e0fb3db227"
+dependencies = [
+ "actix-macros",
+ "actix-threadpool",
+ "copyless",
+ "futures-channel",
+ "futures-util",
+ "smallvec",
+ "tokio",
+]
+
+[[package]]
+name = "actix-server"
+version = "1.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e6d74b464215a473c973a2d7d03a69cc10f4ce1f4b38a7659c5193dc5c675630"
+dependencies = [
+ "actix-codec",
+ "actix-rt",
+ "actix-service",
+ "actix-utils",
+ "futures-channel",
+ "futures-util",
+ "log",
+ "mio",
+ "mio-uds",
+ "num_cpus",
+ "slab",
+ "socket2",
+]
+
+[[package]]
+name = "actix-service"
+version = "1.0.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d3e4fc95dfa7e24171b2d0bb46b85f8ab0e8499e4e3caec691fc4ea65c287564"
+dependencies = [
+ "futures-util",
+ "pin-project",
+]
+
+[[package]]
+name = "actix-testing"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "47239ca38799ab74ee6a8a94d1ce857014b2ac36f242f70f3f75a66f691e791c"
+dependencies = [
+ "actix-macros",
+ "actix-rt",
+ "actix-server",
+ "actix-service",
+ "log",
+ "socket2",
+]
+
+[[package]]
+name = "actix-threadpool"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "91164716d956745c79dcea5e66d2aa04506549958accefcede5368c70f2fd4ff"
+dependencies = [
+ "derive_more",
+ "futures-channel",
+ "lazy_static",
+ "log",
+ "num_cpus",
+ "parking_lot 0.10.2",
+ "threadpool",
+]
+
+[[package]]
+name = "actix-tls"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a4e5b4faaf105e9a6d389c606c298dcdb033061b00d532af9df56ff3a54995a8"
+dependencies = [
+ "actix-codec",
+ "actix-rt",
+ "actix-service",
+ "actix-utils",
+ "derive_more",
+ "either",
+ "futures",
+ "log",
+]
+
+[[package]]
+name = "actix-utils"
+version = "1.0.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fcf8f5631bf01adec2267808f00e228b761c60c0584cc9fa0b5364f41d147f4e"
+dependencies = [
+ "actix-codec",
+ "actix-rt",
+ "actix-service",
+ "bitflags",
+ "bytes",
+ "either",
+ "futures",
+ "log",
+ "pin-project",
+ "slab",
+]
+
+[[package]]
+name = "actix-web"
+version = "2.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3158e822461040822f0dbf1735b9c2ce1f95f93b651d7a7aded00b1efbb1f635"
+dependencies = [
+ "actix-codec",
+ "actix-http",
+ "actix-macros",
+ "actix-router",
+ "actix-rt",
+ "actix-server",
+ "actix-service",
+ "actix-testing",
+ "actix-threadpool",
+ "actix-tls",
+ "actix-utils",
+ "actix-web-codegen",
+ "awc",
+ "bytes",
+ "derive_more",
+ "encoding_rs",
+ "futures",
+ "fxhash",
+ "log",
+ "mime",
+ "net2",
+ "pin-project",
+ "regex",
+ "serde",
+ "serde_json",
+ "serde_urlencoded",
+ "time",
+ "url",
+]
+
+[[package]]
+name = "actix-web-codegen"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a71bf475cbe07281d0b3696abb48212db118e7e23219f13596ce865235ff5766"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "addr2line"
+version = "0.12.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "602d785912f476e480434627e8732e6766b760c045bbf897d9dfaa9f4fbd399c"
+dependencies = [
+ "gimli",
+]
+
+[[package]]
+name = "adler32"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "567b077b825e468cc974f0020d4082ee6e03132512f207ef1a02fd5d00d1f32d"
+
+[[package]]
+name = "ahash"
+version = "0.3.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e8fd72866655d1904d6b0997d0b07ba561047d070fbe29de039031c641b61217"
+
+[[package]]
+name = "aho-corasick"
+version = "0.7.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "043164d8ba5c4c3035fec9bbee8647c0261d788f3474306f93bb65901cae0e86"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "arc-swap"
+version = "0.4.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4d25d88fd6b8041580a654f9d0c581a047baee2b3efee13275f2fc392fc75034"
+
+[[package]]
+name = "async-trait"
+version = "0.1.36"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a265e3abeffdce30b2e26b7a11b222fe37c6067404001b434101457d0385eb92"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "atoi"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e0afb7287b68575f5ca0e5c7e40191cbd4be59d325781f46faa603e176eaef47"
+dependencies = [
+ "num-traits",
+]
+
+[[package]]
+name = "atty"
+version = "0.2.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8"
+dependencies = [
+ "hermit-abi",
+ "libc",
+ "winapi 0.3.8",
+]
+
+[[package]]
+name = "autocfg"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f8aac770f1885fd7e387acedd76065302551364496e46b3dd00860b2f8359b9d"
+
+[[package]]
+name = "awc"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d7601d4d1d7ef2335d6597a41b5fe069f6ab799b85f53565ab390e7b7065aac5"
+dependencies = [
+ "actix-codec",
+ "actix-http",
+ "actix-rt",
+ "actix-service",
+ "base64 0.11.0",
+ "bytes",
+ "derive_more",
+ "futures-core",
+ "log",
+ "mime",
+ "percent-encoding",
+ "rand",
+ "serde",
+ "serde_json",
+ "serde_urlencoded",
+]
+
+[[package]]
+name = "backtrace"
+version = "0.3.49"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "05100821de9e028f12ae3d189176b41ee198341eb8f369956407fea2f5cc666c"
+dependencies = [
+ "addr2line",
+ "cfg-if",
+ "libc",
+ "miniz_oxide",
+ "object",
+ "rustc-demangle",
+]
+
+[[package]]
+name = "base64"
+version = "0.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b41b7ea54a0c9d92199de89e20e58d49f02f8e699814ef3fdf266f6f748d15c7"
+
+[[package]]
+name = "base64"
+version = "0.12.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3441f0f7b02788e948e47f457ca01f1d7e6d92c693bc132c22b087d3141c03ff"
+
+[[package]]
+name = "bitflags"
+version = "1.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693"
+
+[[package]]
+name = "block-buffer"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4"
+dependencies = [
+ "generic-array",
+]
+
+[[package]]
+name = "brotli-sys"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4445dea95f4c2b41cde57cc9fee236ae4dbae88d8fcbdb4750fc1bb5d86aaecd"
+dependencies = [
+ "cc",
+ "libc",
+]
+
+[[package]]
+name = "brotli2"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0cb036c3eade309815c15ddbacec5b22c4d1f3983a774ab2eac2e3e9ea85568e"
+dependencies = [
+ "brotli-sys",
+ "libc",
+]
+
+[[package]]
+name = "bumpalo"
+version = "3.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2e8c087f005730276d1096a652e92a8bacee2e2472bcc9715a74d2bec38b5820"
+
+[[package]]
+name = "byteorder"
+version = "1.3.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "08c48aae112d48ed9f069b33538ea9e3e90aa263cfa3d1c24309612b1f7472de"
+
+[[package]]
+name = "bytes"
+version = "0.5.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "118cf036fbb97d0816e3c34b2d7a1e8cfc60f68fcf63d550ddbe9bd5f59c213b"
+dependencies = [
+ "loom",
+]
+
+[[package]]
+name = "bytestring"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fc7c05fa5172da78a62d9949d662d2ac89d4cc7355d7b49adee5163f1fb3f363"
+dependencies = [
+ "bytes",
+]
+
+[[package]]
+name = "cc"
+version = "1.0.54"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7bbb73db36c1246e9034e307d0fba23f9a2e251faa47ade70c1bd252220c8311"
+
+[[package]]
+name = "cfg-if"
+version = "0.1.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822"
+
+[[package]]
+name = "chrono"
+version = "0.4.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "80094f509cf8b5ae86a4966a39b3ff66cd7e2a3e594accec3743ff3fabeab5b2"
+dependencies = [
+ "num-integer",
+ "num-traits",
+ "serde",
+ "time",
+]
+
+[[package]]
+name = "cloudabi"
+version = "0.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ddfc5b9aa5d4507acaf872de71051dfd0e309860e88966e1051e462a077aac4f"
+dependencies = [
+ "bitflags",
+]
+
+[[package]]
+name = "cloudabi"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4344512281c643ae7638bbabc3af17a11307803ec8f0fcad9fae512a8bf36467"
+dependencies = [
+ "bitflags",
+]
+
+[[package]]
+name = "copyless"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a2df960f5d869b2dd8532793fde43eb5427cceb126c929747a26823ab0eeb536"
+
+[[package]]
+name = "core-foundation"
+version = "0.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "57d24c7a13c43e870e37c1556b74555437870a04514f7685f5b354e090567171"
+dependencies = [
+ "core-foundation-sys",
+ "libc",
+]
+
+[[package]]
+name = "core-foundation-sys"
+version = "0.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b3a71ab494c0b5b860bdc8407ae08978052417070c2ced38573a9157ad75b8ac"
+
+[[package]]
+name = "cpuid-bool"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ec6763c20301ab0dc67051d1b6f4cc9132ad9e6eddcb1f10c6c53ea6d6ae2183"
+
+[[package]]
+name = "crc32fast"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ba125de2af0df55319f41944744ad91c71113bf74a4646efff39afe1f6842db1"
+dependencies = [
+ "cfg-if",
+]
+
+[[package]]
+name = "crossbeam-channel"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cced8691919c02aac3cb0a1bc2e9b73d89e832bf9a06fc579d4e71b68a2da061"
+dependencies = [
+ "crossbeam-utils",
+ "maybe-uninit",
+]
+
+[[package]]
+name = "crossbeam-queue"
+version = "0.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "774ba60a54c213d409d5353bda12d49cd68d14e45036a285234c8d6f91f92570"
+dependencies = [
+ "cfg-if",
+ "crossbeam-utils",
+ "maybe-uninit",
+]
+
+[[package]]
+name = "crossbeam-utils"
+version = "0.7.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c3c7c73a2d1e9fc0886a08b93e98eb643461230d5f1925e4036204d5f2e261a8"
+dependencies = [
+ "autocfg",
+ "cfg-if",
+ "lazy_static",
+]
+
+[[package]]
+name = "crypto-mac"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b584a330336237c1eecd3e94266efb216c56ed91225d634cb2991c5f3fd1aeab"
+dependencies = [
+ "generic-array",
+ "subtle",
+]
+
+[[package]]
+name = "derive_more"
+version = "0.99.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bc655351f820d774679da6cdc23355a93de496867d8203496675162e17b1d671"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "digest"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066"
+dependencies = [
+ "generic-array",
+]
+
+[[package]]
+name = "dotenv"
+version = "0.15.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "77c90badedccf4105eca100756a0b1289e191f6fcbdadd3cee1d2f614f97da8f"
+
+[[package]]
+name = "dtoa"
+version = "0.4.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "134951f4028bdadb9b84baf4232681efbf277da25144b9b0ad65df75946c422b"
+
+[[package]]
+name = "either"
+version = "1.5.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bb1f6b1ce1c140482ea30ddd3335fc0024ac7ee112895426e0a629a6c20adfe3"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "encoding_rs"
+version = "0.8.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e8ac63f94732332f44fe654443c46f6375d1939684c17b0afb6cb56b0456e171"
+dependencies = [
+ "cfg-if",
+]
+
+[[package]]
+name = "enum-as-inner"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bc4bfcfacb61d231109d1d55202c1f33263319668b168843e02ad4652725ec9c"
+dependencies = [
+ "heck",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "env_logger"
+version = "0.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "44533bbbb3bb3c1fa17d9f2e4e38bbbaf8396ba82193c4cb1b6445d711445d36"
+dependencies = [
+ "atty",
+ "humantime",
+ "log",
+ "regex",
+ "termcolor",
+]
+
+[[package]]
+name = "failure"
+version = "0.1.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d32e9bd16cc02eae7db7ef620b392808b89f6a5e16bb3497d159c6b92a0f4f86"
+dependencies = [
+ "backtrace",
+ "failure_derive",
+]
+
+[[package]]
+name = "failure_derive"
+version = "0.1.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "aa4da3c766cd7a0db8242e326e9e4e081edd567072893ed320008189715366a4"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+ "synstructure",
+]
+
+[[package]]
+name = "flate2"
+version = "1.0.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2cfff41391129e0a856d6d822600b8d71179d46879e310417eb9c762eb178b42"
+dependencies = [
+ "cfg-if",
+ "crc32fast",
+ "libc",
+ "miniz_oxide",
+]
+
+[[package]]
+name = "fnv"
+version = "1.0.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
+
+[[package]]
+name = "foreign-types"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1"
+dependencies = [
+ "foreign-types-shared",
+]
+
+[[package]]
+name = "foreign-types-shared"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"
+
+[[package]]
+name = "fuchsia-zircon"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2e9763c69ebaae630ba35f74888db465e49e259ba1bc0eda7d06f4a067615d82"
+dependencies = [
+ "bitflags",
+ "fuchsia-zircon-sys",
+]
+
+[[package]]
+name = "fuchsia-zircon-sys"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3dcaa9ae7725d12cdb85b3ad99a434db70b468c09ded17e012d86b5c1010f7a7"
+
+[[package]]
+name = "futures"
+version = "0.3.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e05b85ec287aac0dc34db7d4a569323df697f9c55b99b15d6b4ef8cde49f613"
+dependencies = [
+ "futures-channel",
+ "futures-core",
+ "futures-executor",
+ "futures-io",
+ "futures-sink",
+ "futures-task",
+ "futures-util",
+]
+
+[[package]]
+name = "futures-channel"
+version = "0.3.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f366ad74c28cca6ba456d95e6422883cfb4b252a83bed929c83abfdbbf2967d5"
+dependencies = [
+ "futures-core",
+ "futures-sink",
+]
+
+[[package]]
+name = "futures-core"
+version = "0.3.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "59f5fff90fd5d971f936ad674802482ba441b6f09ba5e15fd8b39145582ca399"
+
+[[package]]
+name = "futures-executor"
+version = "0.3.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "10d6bb888be1153d3abeb9006b11b02cf5e9b209fda28693c31ae1e4e012e314"
+dependencies = [
+ "futures-core",
+ "futures-task",
+ "futures-util",
+]
+
+[[package]]
+name = "futures-io"
+version = "0.3.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "de27142b013a8e869c14957e6d2edeef89e97c289e69d042ee3a49acd8b51789"
+
+[[package]]
+name = "futures-macro"
+version = "0.3.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d0b5a30a4328ab5473878237c447333c093297bded83a4983d10f4deea240d39"
+dependencies = [
+ "proc-macro-hack",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "futures-sink"
+version = "0.3.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3f2032893cb734c7a05d85ce0cc8b8c4075278e93b24b66f9de99d6eb0fa8acc"
+
+[[package]]
+name = "futures-task"
+version = "0.3.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bdb66b5f09e22019b1ab0830f7785bcea8e7a42148683f99214f73f8ec21a626"
+dependencies = [
+ "once_cell",
+]
+
+[[package]]
+name = "futures-timer"
+version = "3.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e64b03909df88034c26dc1547e8970b91f98bdb65165d6a4e9110d94263dbb2c"
+
+[[package]]
+name = "futures-util"
+version = "0.3.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8764574ff08b701a084482c3c7031349104b07ac897393010494beaa18ce32c6"
+dependencies = [
+ "futures-channel",
+ "futures-core",
+ "futures-io",
+ "futures-macro",
+ "futures-sink",
+ "futures-task",
+ "memchr",
+ "pin-project",
+ "pin-utils",
+ "proc-macro-hack",
+ "proc-macro-nested",
+ "slab",
+]
+
+[[package]]
+name = "fxhash"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c"
+dependencies = [
+ "byteorder",
+]
+
+[[package]]
+name = "generator"
+version = "0.6.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "add72f17bb81521258fcc8a7a3245b1e184e916bfbe34f0ea89558f440df5c68"
+dependencies = [
+ "cc",
+ "libc",
+ "log",
+ "rustc_version",
+ "winapi 0.3.8",
+]
+
+[[package]]
+name = "generic-array"
+version = "0.14.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "60fb4bb6bba52f78a471264d9a3b7d026cc0af47b22cd2cffbc0b787ca003e63"
+dependencies = [
+ "typenum",
+ "version_check 0.9.2",
+]
+
+[[package]]
+name = "getrandom"
+version = "0.1.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7abc8dd8451921606d809ba32e95b6111925cd2906060d2dcc29c070220503eb"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "wasi",
+]
+
+[[package]]
+name = "gimli"
+version = "0.21.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bcc8e0c9bce37868955864dbecd2b1ab2bdf967e6f28066d65aaac620444b65c"
+
+[[package]]
+name = "h2"
+version = "0.2.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "79b7246d7e4b979c03fa093da39cfb3617a96bbeee6310af63991668d7e843ff"
+dependencies = [
+ "bytes",
+ "fnv",
+ "futures-core",
+ "futures-sink",
+ "futures-util",
+ "http",
+ "indexmap",
+ "log",
+ "slab",
+ "tokio",
+ "tokio-util 0.3.1",
+]
+
+[[package]]
+name = "hashbrown"
+version = "0.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "34f595585f103464d8d2f6e9864682d74c1601fed5e07d62b1c9058dba8246fb"
+dependencies = [
+ "ahash",
+ "autocfg",
+]
+
+[[package]]
+name = "heck"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "20564e78d53d2bb135c343b3f47714a56af2061f1c928fdb541dc7b9fdd94205"
+dependencies = [
+ "unicode-segmentation",
+]
+
+[[package]]
+name = "hermit-abi"
+version = "0.1.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b9586eedd4ce6b3c498bc3b4dd92fc9f11166aa908a914071953768066c67909"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "hex"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "644f9158b2f133fd50f5fb3242878846d9eb792e445c893805ff0e3824006e35"
+
+[[package]]
+name = "hmac"
+version = "0.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "126888268dcc288495a26bf004b38c5fdbb31682f992c84ceb046a1f0fe38840"
+dependencies = [
+ "crypto-mac",
+ "digest",
+]
+
+[[package]]
+name = "hostname"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3c731c3e10504cc8ed35cfe2f1db4c9274c3d35fa486e3b31df46f068ef3e867"
+dependencies = [
+ "libc",
+ "match_cfg",
+ "winapi 0.3.8",
+]
+
+[[package]]
+name = "http"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "28d569972648b2c512421b5f2a405ad6ac9666547189d0c5477a3f200f3e02f9"
+dependencies = [
+ "bytes",
+ "fnv",
+ "itoa",
+]
+
+[[package]]
+name = "http-body"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "13d5ff830006f7646652e057693569bfe0d51760c0085a071769d142a205111b"
+dependencies = [
+ "bytes",
+ "http",
+]
+
+[[package]]
+name = "httparse"
+version = "1.3.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cd179ae861f0c2e53da70d892f5f3029f9594be0c41dc5269cd371691b1dc2f9"
+
+[[package]]
+name = "humantime"
+version = "1.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "df004cfca50ef23c36850aaaa59ad52cc70d0e90243c3c7737a4dd32dc7a3c4f"
+dependencies = [
+ "quick-error",
+]
+
+[[package]]
+name = "hyper"
+version = "0.13.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a6e7655b9594024ad0ee439f3b5a7299369dc2a3f459b47c696f9ff676f9aa1f"
+dependencies = [
+ "bytes",
+ "futures-channel",
+ "futures-core",
+ "futures-util",
+ "h2",
+ "http",
+ "http-body",
+ "httparse",
+ "itoa",
+ "log",
+ "pin-project",
+ "socket2",
+ "time",
+ "tokio",
+ "tower-service",
+ "want",
+]
+
+[[package]]
+name = "hyper-tls"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3adcd308402b9553630734e9c36b77a7e48b3821251ca2493e8cd596763aafaa"
+dependencies = [
+ "bytes",
+ "hyper",
+ "native-tls",
+ "tokio",
+ "tokio-tls",
+]
+
+[[package]]
+name = "idna"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "02e2673c30ee86b5b96a9cb52ad15718aa1f966f5ab9ad54a8b95d5ca33120a9"
+dependencies = [
+ "matches",
+ "unicode-bidi",
+ "unicode-normalization",
+]
+
+[[package]]
+name = "indexmap"
+version = "1.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c398b2b113b55809ceb9ee3e753fcbac793f1956663f3c36549c1346015c2afe"
+dependencies = [
+ "autocfg",
+]
+
+[[package]]
+name = "instant"
+version = "0.1.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5b141fdc7836c525d4d594027d318c84161ca17aaf8113ab1f81ab93ae897485"
+
+[[package]]
+name = "iovec"
+version = "0.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b2b3ea6ff95e175473f8ffe6a7eb7c00d054240321b84c57051175fe3c1e075e"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "ipconfig"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f7e2f18aece9709094573a9f24f483c4f65caa4298e2f7ae1b71cc65d853fad7"
+dependencies = [
+ "socket2",
+ "widestring",
+ "winapi 0.3.8",
+ "winreg 0.6.2",
+]
+
+[[package]]
+name = "itoa"
+version = "0.4.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dc6f3ad7b9d11a0c00842ff8de1b60ee58661048eb8049ed33c73594f359d7e6"
+
+[[package]]
+name = "js-sys"
+version = "0.3.40"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ce10c23ad2ea25ceca0093bd3192229da4c5b3c0f2de499c1ecac0d98d452177"
+dependencies = [
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "kernel32-sys"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7507624b29483431c0ba2d82aece8ca6cdba9382bff4ddd0f7490560c056098d"
+dependencies = [
+ "winapi 0.2.8",
+ "winapi-build",
+]
+
+[[package]]
+name = "labrinth"
+version = "0.1.0"
+dependencies = [
+ "actix-files",
+ "actix-multipart",
+ "actix-rt",
+ "actix-web",
+ "async-trait",
+ "base64 0.12.3",
+ "chrono",
+ "dotenv",
+ "env_logger",
+ "futures",
+ "futures-timer",
+ "log",
+ "meilisearch-sdk",
+ "rand",
+ "reqwest",
+ "serde",
+ "serde_json",
+ "sha1",
+ "sqlx",
+ "thiserror",
+]
+
+[[package]]
+name = "language-tags"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a91d884b6667cd606bb5a69aa0c99ba811a115fc68915e7056ec08a46e93199a"
+
+[[package]]
+name = "lazy_static"
+version = "1.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
+
+[[package]]
+name = "libc"
+version = "0.2.71"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9457b06509d27052635f90d6466700c65095fdf75409b3fbdd903e988b886f49"
+
+[[package]]
+name = "linked-hash-map"
+version = "0.5.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8dd5a6d5999d9907cda8ed67bbd137d3af8085216c2ac62de5be860bd41f304a"
+
+[[package]]
+name = "lock_api"
+version = "0.3.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c4da24a77a3d8a6d4862d95f72e6fdb9c09a643ecdb402d754004a557f2bec75"
+dependencies = [
+ "scopeguard",
+]
+
+[[package]]
+name = "lock_api"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "28247cc5a5be2f05fbcd76dd0cf2c7d3b5400cb978a28042abcd4fa0b3f8261c"
+dependencies = [
+ "scopeguard",
+]
+
+[[package]]
+name = "log"
+version = "0.4.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "14b6052be84e6b71ab17edffc2eeabf5c2c3ae1fdb464aae35ac50c67a44e1f7"
+dependencies = [
+ "cfg-if",
+]
+
+[[package]]
+name = "loom"
+version = "0.3.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4ecc775857611e1df29abba5c41355cdf540e7e9d4acfdf0f355eefee82330b7"
+dependencies = [
+ "cfg-if",
+ "generator",
+ "scoped-tls",
+]
+
+[[package]]
+name = "lru-cache"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "31e24f1ad8321ca0e8a1e0ac13f23cb668e6f5466c2c57319f6a5cf1cc8e3b1c"
+dependencies = [
+ "linked-hash-map",
+]
+
+[[package]]
+name = "maplit"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3e2e65a1a2e43cfcb47a895c4c8b10d1f4a61097f9f254f183aee60cad9c651d"
+
+[[package]]
+name = "match_cfg"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ffbee8634e0d45d258acb448e7eaab3fce7a0a467395d4d9f228e3c1f01fb2e4"
+
+[[package]]
+name = "matches"
+version = "0.1.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7ffc5c5338469d4d3ea17d269fa8ea3512ad247247c30bd2df69e68309ed0a08"
+
+[[package]]
+name = "maybe-uninit"
+version = "2.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "60302e4db3a61da70c0cb7991976248362f30319e88850c487b9b95bbf059e00"
+
+[[package]]
+name = "md-5"
+version = "0.9.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7b5a279bb9607f9f53c22d496eade00d138d1bdcccd07d74650387cf94942a15"
+dependencies = [
+ "block-buffer",
+ "digest",
+ "opaque-debug",
+]
+
+[[package]]
+name = "meilisearch-sdk"
+version = "0.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e99e00b31a0e33add02a01c969a108144882ef27915d81292deda71baa6b6eea"
+dependencies = [
+ "log",
+ "minreq",
+ "serde",
+ "serde_json",
+ "urlencoding",
+ "wasm-bindgen",
+ "wasm-bindgen-futures",
+ "web-sys",
+]
+
+[[package]]
+name = "memchr"
+version = "2.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3728d817d99e5ac407411fa471ff9800a778d88a24685968b36824eaf4bee400"
+
+[[package]]
+name = "mime"
+version = "0.3.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d"
+
+[[package]]
+name = "mime_guess"
+version = "2.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2684d4c2e97d99848d30b324b00c8fcc7e5c897b7cbb5819b09e7c90e8baf212"
+dependencies = [
+ "mime",
+ "unicase",
+]
+
+[[package]]
+name = "miniz_oxide"
+version = "0.3.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "791daaae1ed6889560f8c4359194f56648355540573244a5448a83ba1ecc7435"
+dependencies = [
+ "adler32",
+]
+
+[[package]]
+name = "minreq"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ab229c252995e9d56cc66857f3ab2c41e3138b1a6c92089f013698388e64d6bd"
+
+[[package]]
+name = "mio"
+version = "0.6.22"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fce347092656428bc8eaf6201042cb551b8d67855af7374542a92a0fbfcac430"
+dependencies = [
+ "cfg-if",
+ "fuchsia-zircon",
+ "fuchsia-zircon-sys",
+ "iovec",
+ "kernel32-sys",
+ "libc",
+ "log",
+ "miow",
+ "net2",
+ "slab",
+ "winapi 0.2.8",
+]
+
+[[package]]
+name = "mio-uds"
+version = "0.6.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "afcb699eb26d4332647cc848492bbc15eafb26f08d0304550d5aa1f612e066f0"
+dependencies = [
+ "iovec",
+ "libc",
+ "mio",
+]
+
+[[package]]
+name = "miow"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8c1f2f3b1cf331de6896aabf6e9d55dca90356cc9960cca7eaaf408a355ae919"
+dependencies = [
+ "kernel32-sys",
+ "net2",
+ "winapi 0.2.8",
+ "ws2_32-sys",
+]
+
+[[package]]
+name = "modrinth"
+version = "0.1.0"
+dependencies = [
+ "actix-files",
+ "actix-multipart",
+ "actix-rt",
+ "actix-web",
+ "async-trait",
+ "base64 0.12.3",
+ "chrono",
+ "dotenv",
+ "env_logger",
+ "futures",
+ "futures-timer",
+ "log",
+ "meilisearch-sdk",
+ "rand",
+ "reqwest",
+ "serde",
+ "serde_json",
+ "sha1",
+ "sqlx",
+ "sqlx-macros",
+ "thiserror",
+]
+
+[[package]]
+name = "native-tls"
+version = "0.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2b0d88c06fe90d5ee94048ba40409ef1d9315d86f6f38c2efdaad4fb50c58b2d"
+dependencies = [
+ "lazy_static",
+ "libc",
+ "log",
+ "openssl",
+ "openssl-probe",
+ "openssl-sys",
+ "schannel",
+ "security-framework",
+ "security-framework-sys",
+ "tempfile",
+]
+
+[[package]]
+name = "net2"
+version = "0.2.34"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2ba7c918ac76704fb42afcbbb43891e72731f3dcca3bef2a19786297baf14af7"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "winapi 0.3.8",
+]
+
+[[package]]
+name = "nom"
+version = "4.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2ad2a91a8e869eeb30b9cb3119ae87773a8f4ae617f41b1eb9c154b2905f7bd6"
+dependencies = [
+ "memchr",
+ "version_check 0.1.5",
+]
+
+[[package]]
+name = "num-integer"
+version = "0.1.43"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8d59457e662d541ba17869cf51cf177c0b5f0cbf476c66bdc90bf1edac4f875b"
+dependencies = [
+ "autocfg",
+ "num-traits",
+]
+
+[[package]]
+name = "num-traits"
+version = "0.2.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ac267bcc07f48ee5f8935ab0d24f316fb722d7a1292e2913f0cc196b29ffd611"
+dependencies = [
+ "autocfg",
+]
+
+[[package]]
+name = "num_cpus"
+version = "1.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "05499f3756671c15885fee9034446956fff3f243d6077b91e5767df161f766b3"
+dependencies = [
+ "hermit-abi",
+ "libc",
+]
+
+[[package]]
+name = "object"
+version = "0.20.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1ab52be62400ca80aa00285d25253d7f7c437b7375c4de678f5405d3afe82ca5"
+
+[[package]]
+name = "once_cell"
+version = "1.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b631f7e854af39a1739f401cf34a8a013dfe09eac4fa4dba91e9768bd28168d"
+
+[[package]]
+name = "opaque-debug"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5"
+
+[[package]]
+name = "openssl"
+version = "0.10.29"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cee6d85f4cb4c4f59a6a85d5b68a233d280c82e29e822913b9c8b129fbf20bdd"
+dependencies = [
+ "bitflags",
+ "cfg-if",
+ "foreign-types",
+ "lazy_static",
+ "libc",
+ "openssl-sys",
+]
+
+[[package]]
+name = "openssl-probe"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "77af24da69f9d9341038eba93a073b1fdaaa1b788221b00a69bce9e762cb32de"
+
+[[package]]
+name = "openssl-sys"
+version = "0.9.58"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a842db4709b604f0fe5d1170ae3565899be2ad3d9cbc72dedc789ac0511f78de"
+dependencies = [
+ "autocfg",
+ "cc",
+ "libc",
+ "pkg-config",
+ "vcpkg",
+]
+
+[[package]]
+name = "parking_lot"
+version = "0.10.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d3a704eb390aafdc107b0e392f56a82b668e3a71366993b5340f5833fd62505e"
+dependencies = [
+ "lock_api 0.3.4",
+ "parking_lot_core 0.7.2",
+]
+
+[[package]]
+name = "parking_lot"
+version = "0.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a4893845fa2ca272e647da5d0e46660a314ead9c2fdd9a883aabc32e481a8733"
+dependencies = [
+ "instant",
+ "lock_api 0.4.1",
+ "parking_lot_core 0.8.0",
+]
+
+[[package]]
+name = "parking_lot_core"
+version = "0.7.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d58c7c768d4ba344e3e8d72518ac13e259d7c7ade24167003b8488e10b6740a3"
+dependencies = [
+ "cfg-if",
+ "cloudabi 0.0.3",
+ "libc",
+ "redox_syscall",
+ "smallvec",
+ "winapi 0.3.8",
+]
+
+[[package]]
+name = "parking_lot_core"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c361aa727dd08437f2f1447be8b59a33b0edd15e0fcee698f935613d9efbca9b"
+dependencies = [
+ "cfg-if",
+ "cloudabi 0.1.0",
+ "instant",
+ "libc",
+ "redox_syscall",
+ "smallvec",
+ "winapi 0.3.8",
+]
+
+[[package]]
+name = "percent-encoding"
+version = "2.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e"
+
+[[package]]
+name = "phf"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3dfb61232e34fcb633f43d12c58f83c1df82962dcdfa565a4e866ffc17dafe12"
+dependencies = [
+ "phf_macros",
+ "phf_shared",
+ "proc-macro-hack",
+]
+
+[[package]]
+name = "phf_generator"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "17367f0cc86f2d25802b2c26ee58a7b23faeccf78a396094c13dced0d0182526"
+dependencies = [
+ "phf_shared",
+ "rand",
+]
+
+[[package]]
+name = "phf_macros"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7f6fde18ff429ffc8fe78e2bf7f8b7a5a5a6e2a8b58bc5a9ac69198bbda9189c"
+dependencies = [
+ "phf_generator",
+ "phf_shared",
+ "proc-macro-hack",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "phf_shared"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c00cf8b9eafe68dde5e9eaa2cef8ee84a9336a47d566ec55ca16589633b65af7"
+dependencies = [
+ "siphasher",
+]
+
+[[package]]
+name = "pin-project"
+version = "0.4.22"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "12e3a6cdbfe94a5e4572812a0201f8c0ed98c1c452c7b8563ce2276988ef9c17"
+dependencies = [
+ "pin-project-internal",
+]
+
+[[package]]
+name = "pin-project-internal"
+version = "0.4.22"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6a0ffd45cf79d88737d7cc85bfd5d2894bee1139b356e616fe85dc389c61aaf7"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "pin-project-lite"
+version = "0.1.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "282adbf10f2698a7a77f8e983a74b2d18176c19a7fd32a45446139ae7b02b715"
+
+[[package]]
+name = "pin-utils"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
+
+[[package]]
+name = "pkg-config"
+version = "0.3.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "05da548ad6865900e60eaba7f589cc0783590a92e940c26953ff81ddbab2d677"
+
+[[package]]
+name = "ppv-lite86"
+version = "0.2.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "237a5ed80e274dbc66f86bd59c1e25edc039660be53194b5fe0a482e0f2612ea"
+
+[[package]]
+name = "proc-macro-hack"
+version = "0.5.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7e0456befd48169b9f13ef0f0ad46d492cf9d2dbb918bcf38e01eed4ce3ec5e4"
+
+[[package]]
+name = "proc-macro-nested"
+version = "0.1.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eba180dafb9038b050a4c280019bbedf9f2467b61e5d892dcad585bb57aadc5a"
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "beae6331a816b1f65d04c45b078fd8e6c93e8071771f41b8163255bbd8d7c8fa"
+dependencies = [
+ "unicode-xid",
+]
+
+[[package]]
+name = "quick-error"
+version = "1.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0"
+
+[[package]]
+name = "quote"
+version = "1.0.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "aa563d17ecb180e500da1cfd2b028310ac758de548efdd203e18f283af693f37"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
+name = "rand"
+version = "0.7.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03"
+dependencies = [
+ "getrandom",
+ "libc",
+ "rand_chacha",
+ "rand_core",
+ "rand_hc",
+ "rand_pcg",
+]
+
+[[package]]
+name = "rand_chacha"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402"
+dependencies = [
+ "ppv-lite86",
+ "rand_core",
+]
+
+[[package]]
+name = "rand_core"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19"
+dependencies = [
+ "getrandom",
+]
+
+[[package]]
+name = "rand_hc"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c"
+dependencies = [
+ "rand_core",
+]
+
+[[package]]
+name = "rand_pcg"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "16abd0c1b639e9eb4d7c50c0b8100b0d0f849be2349829c740fe8e6eb4816429"
+dependencies = [
+ "rand_core",
+]
+
+[[package]]
+name = "redox_syscall"
+version = "0.1.56"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2439c63f3f6139d1b57529d16bc3b8bb855230c8efcc5d3a896c8bea7c3b1e84"
+
+[[package]]
+name = "regex"
+version = "1.3.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9c3780fcf44b193bc4d09f36d2a3c87b251da4a046c87795a0d35f4f927ad8e6"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-syntax",
+ "thread_local",
+]
+
+[[package]]
+name = "regex-syntax"
+version = "0.6.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "26412eb97c6b088a6997e05f69403a802a92d520de2f8e63c2b65f9e0f47c4e8"
+
+[[package]]
+name = "remove_dir_all"
+version = "0.5.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7"
+dependencies = [
+ "winapi 0.3.8",
+]
+
+[[package]]
+name = "reqwest"
+version = "0.10.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3b82c9238b305f26f53443e3a4bc8528d64b8d0bee408ec949eb7bf5635ec680"
+dependencies = [
+ "base64 0.12.3",
+ "bytes",
+ "encoding_rs",
+ "futures-core",
+ "futures-util",
+ "http",
+ "http-body",
+ "hyper",
+ "hyper-tls",
+ "js-sys",
+ "lazy_static",
+ "log",
+ "mime",
+ "mime_guess",
+ "native-tls",
+ "percent-encoding",
+ "pin-project-lite",
+ "serde",
+ "serde_json",
+ "serde_urlencoded",
+ "tokio",
+ "tokio-tls",
+ "url",
+ "wasm-bindgen",
+ "wasm-bindgen-futures",
+ "web-sys",
+ "winreg 0.7.0",
+]
+
+[[package]]
+name = "resolv-conf"
+version = "0.6.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "11834e137f3b14e309437a8276714eed3a80d1ef894869e510f2c0c0b98b9f4a"
+dependencies = [
+ "hostname",
+ "quick-error",
+]
+
+[[package]]
+name = "rustc-demangle"
+version = "0.1.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4c691c0e608126e00913e33f0ccf3727d5fc84573623b8d65b2df340b5201783"
+
+[[package]]
+name = "rustc_version"
+version = "0.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "138e3e0acb6c9fb258b19b67cb8abd63c00679d2851805ea151465464fe9030a"
+dependencies = [
+ "semver",
+]
+
+[[package]]
+name = "ryu"
+version = "1.0.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "71d301d4193d031abdd79ff7e3dd721168a9572ef3fe51a1517aba235bd8f86e"
+
+[[package]]
+name = "schannel"
+version = "0.1.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f05ba609c234e60bee0d547fe94a4c7e9da733d1c962cf6e59efa4cd9c8bc75"
+dependencies = [
+ "lazy_static",
+ "winapi 0.3.8",
+]
+
+[[package]]
+name = "scoped-tls"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "332ffa32bf586782a3efaeb58f127980944bbc8c4d6913a86107ac2a5ab24b28"
+
+[[package]]
+name = "scopeguard"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd"
+
+[[package]]
+name = "security-framework"
+version = "0.4.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "64808902d7d99f78eaddd2b4e2509713babc3dc3c85ad6f4c447680f3c01e535"
+dependencies = [
+ "bitflags",
+ "core-foundation",
+ "core-foundation-sys",
+ "libc",
+ "security-framework-sys",
+]
+
+[[package]]
+name = "security-framework-sys"
+version = "0.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "17bf11d99252f512695eb468de5516e5cf75455521e69dfe343f3b74e4748405"
+dependencies = [
+ "core-foundation-sys",
+ "libc",
+]
+
+[[package]]
+name = "semver"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1d7eb9ef2c18661902cc47e535f9bc51b78acd254da71d375c2f6720d9a40403"
+dependencies = [
+ "semver-parser",
+]
+
+[[package]]
+name = "semver-parser"
+version = "0.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3"
+
+[[package]]
+name = "serde"
+version = "1.0.114"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5317f7588f0a5078ee60ef675ef96735a1442132dc645eb1d12c018620ed8cd3"
+dependencies = [
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_derive"
+version = "1.0.114"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2a0be94b04690fbaed37cddffc5c134bf537c8e3329d53e982fe04c374978f8e"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "serde_json"
+version = "1.0.55"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ec2c5d7e739bc07a3e73381a39d61fdb5f671c60c1df26a130690665803d8226"
+dependencies = [
+ "indexmap",
+ "itoa",
+ "ryu",
+ "serde",
+]
+
+[[package]]
+name = "serde_urlencoded"
+version = "0.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9ec5d77e2d4c73717816afac02670d5c4f534ea95ed430442cad02e7a6e32c97"
+dependencies = [
+ "dtoa",
+ "itoa",
+ "serde",
+ "url",
+]
+
+[[package]]
+name = "sha-1"
+version = "0.9.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "170a36ea86c864a3f16dd2687712dd6646f7019f301e57537c7f4dc9f5916770"
+dependencies = [
+ "block-buffer",
+ "cfg-if",
+ "cpuid-bool",
+ "digest",
+ "opaque-debug",
+]
+
+[[package]]
+name = "sha1"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2579985fda508104f7587689507983eadd6a6e84dd35d6d115361f530916fa0d"
+
+[[package]]
+name = "sha2"
+version = "0.9.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2933378ddfeda7ea26f48c555bdad8bb446bf8a3d17832dc83e380d444cfb8c1"
+dependencies = [
+ "block-buffer",
+ "cfg-if",
+ "cpuid-bool",
+ "digest",
+ "opaque-debug",
+]
+
+[[package]]
+name = "signal-hook-registry"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "94f478ede9f64724c5d173d7bb56099ec3e2d9fc2774aac65d34b8b890405f41"
+dependencies = [
+ "arc-swap",
+ "libc",
+]
+
+[[package]]
+name = "siphasher"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fa8f3741c7372e75519bd9346068370c9cdaabcc1f9599cbcf2a2719352286b7"
+
+[[package]]
+name = "slab"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c111b5bd5695e56cffe5129854aa230b39c93a305372fdbb2668ca2394eea9f8"
+
+[[package]]
+name = "smallvec"
+version = "1.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c7cb5678e1615754284ec264d9bb5b4c27d2018577fd90ac0ceb578591ed5ee4"
+
+[[package]]
+name = "socket2"
+version = "0.3.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "03088793f677dce356f3ccc2edb1b314ad191ab702a5de3faf49304f7e104918"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "redox_syscall",
+ "winapi 0.3.8",
+]
+
+[[package]]
+name = "sqlformat"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5ce64a4576e1720a2e511bf3ccdb8c0f6cfed0fc265bcbaa0bd369485e02c631"
+dependencies = [
+ "lazy_static",
+ "maplit",
+ "regex",
+]
+
+[[package]]
+name = "sqlx"
+version = "0.4.0-pre"
+source = "git+https://github.com/launchbadge/sqlx/#41261862d3537bd386149b4bf40d3a9517f73205"
+dependencies = [
+ "sqlx-core",
+ "sqlx-macros",
+]
+
+[[package]]
+name = "sqlx-core"
+version = "0.4.0-pre"
+source = "git+https://github.com/launchbadge/sqlx/#41261862d3537bd386149b4bf40d3a9517f73205"
+dependencies = [
+ "atoi",
+ "base64 0.12.3",
+ "bitflags",
+ "byteorder",
+ "bytes",
+ "chrono",
+ "crossbeam-channel",
+ "crossbeam-queue",
+ "crossbeam-utils",
+ "either",
+ "futures-channel",
+ "futures-core",
+ "futures-util",
+ "hashbrown",
+ "hex",
+ "hmac",
+ "itoa",
+ "libc",
+ "log",
+ "lru-cache",
+ "md-5",
+ "memchr",
+ "once_cell",
+ "parking_lot 0.11.0",
+ "percent-encoding",
+ "phf",
+ "rand",
+ "serde",
+ "sha-1",
+ "sha2",
+ "smallvec",
+ "sqlformat",
+ "sqlx-rt",
+ "stringprep",
+ "thiserror",
+ "url",
+ "whoami",
+]
+
+[[package]]
+name = "sqlx-macros"
+version = "0.4.0-pre"
+source = "git+https://github.com/launchbadge/sqlx/#41261862d3537bd386149b4bf40d3a9517f73205"
+dependencies = [
+ "dotenv",
+ "either",
+ "futures",
+ "heck",
+ "hex",
+ "proc-macro2",
+ "quote",
+ "serde",
+ "serde_json",
+ "sha2",
+ "sqlx-core",
+ "sqlx-rt",
+ "syn",
+ "url",
+]
+
+[[package]]
+name = "sqlx-rt"
+version = "0.1.0-pre"
+source = "git+https://github.com/launchbadge/sqlx/#41261862d3537bd386149b4bf40d3a9517f73205"
+dependencies = [
+ "actix-rt",
+ "actix-threadpool",
+ "native-tls",
+ "once_cell",
+ "tokio",
+ "tokio-native-tls",
+]
+
+[[package]]
+name = "stringprep"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8ee348cb74b87454fff4b551cbf727025810a004f88aeacae7f85b87f4e9a1c1"
+dependencies = [
+ "unicode-bidi",
+ "unicode-normalization",
+]
+
+[[package]]
+name = "subtle"
+version = "2.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "502d53007c02d7605a05df1c1a73ee436952781653da5d0bf57ad608f66932c1"
+
+[[package]]
+name = "syn"
+version = "1.0.33"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e8d5d96e8cbb005d6959f119f773bfaebb5684296108fb32600c00cde305b2cd"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-xid",
+]
+
+[[package]]
+name = "synstructure"
+version = "0.12.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b834f2d66f734cb897113e34aaff2f1ab4719ca946f9a7358dba8f8064148701"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+ "unicode-xid",
+]
+
+[[package]]
+name = "tempfile"
+version = "3.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7a6e24d9338a0a5be79593e2fa15a648add6138caa803e2d5bc782c371732ca9"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "rand",
+ "redox_syscall",
+ "remove_dir_all",
+ "winapi 0.3.8",
+]
+
+[[package]]
+name = "termcolor"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bb6bfa289a4d7c5766392812c0a1f4c1ba45afa1ad47803c11e1f407d846d75f"
+dependencies = [
+ "winapi-util",
+]
+
+[[package]]
+name = "thiserror"
+version = "1.0.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7dfdd070ccd8ccb78f4ad66bf1982dc37f620ef696c6b5028fe2ed83dd3d0d08"
+dependencies = [
+ "thiserror-impl",
+]
+
+[[package]]
+name = "thiserror-impl"
+version = "1.0.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bd80fc12f73063ac132ac92aceea36734f04a1d93c1240c6944e23a3b8841793"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "thread_local"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d40c6d1b69745a6ec6fb1ca717914848da4b44ae29d9b3080cbee91d72a69b14"
+dependencies = [
+ "lazy_static",
+]
+
+[[package]]
+name = "threadpool"
+version = "1.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d050e60b33d41c19108b32cea32164033a9013fe3b46cbd4457559bfbf77afaa"
+dependencies = [
+ "num_cpus",
+]
+
+[[package]]
+name = "time"
+version = "0.1.43"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ca8a50ef2360fbd1eeb0ecd46795a87a19024eb4b53c5dc916ca1fd95fe62438"
+dependencies = [
+ "libc",
+ "winapi 0.3.8",
+]
+
+[[package]]
+name = "tinyvec"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "53953d2d3a5ad81d9f844a32f14ebb121f50b650cd59d0ee2a07cf13c617efed"
+
+[[package]]
+name = "tokio"
+version = "0.2.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d099fa27b9702bed751524694adbe393e18b36b204da91eb1cbbbbb4a5ee2d58"
+dependencies = [
+ "bytes",
+ "fnv",
+ "futures-core",
+ "iovec",
+ "lazy_static",
+ "libc",
+ "memchr",
+ "mio",
+ "mio-uds",
+ "num_cpus",
+ "pin-project-lite",
+ "signal-hook-registry",
+ "slab",
+ "tokio-macros",
+ "winapi 0.3.8",
+]
+
+[[package]]
+name = "tokio-macros"
+version = "0.2.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f0c3acc6aa564495a0f2e1d59fab677cd7f81a19994cfc7f3ad0e64301560389"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "tokio-native-tls"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cd608593a919a8e05a7d1fc6df885e40f6a88d3a70a3a7eff23ff27964eda069"
+dependencies = [
+ "native-tls",
+ "tokio",
+]
+
+[[package]]
+name = "tokio-tls"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9a70f4fcd7b3b24fb194f837560168208f669ca8cb70d0c4b862944452396343"
+dependencies = [
+ "native-tls",
+ "tokio",
+]
+
+[[package]]
+name = "tokio-util"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "571da51182ec208780505a32528fc5512a8fe1443ab960b3f2f3ef093cd16930"
+dependencies = [
+ "bytes",
+ "futures-core",
+ "futures-sink",
+ "log",
+ "pin-project-lite",
+ "tokio",
+]
+
+[[package]]
+name = "tokio-util"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "be8242891f2b6cbef26a2d7e8605133c2c554cd35b3e4948ea892d6d68436499"
+dependencies = [
+ "bytes",
+ "futures-core",
+ "futures-sink",
+ "log",
+ "pin-project-lite",
+ "tokio",
+]
+
+[[package]]
+name = "tower-service"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e987b6bf443f4b5b3b6f38704195592cca41c5bb7aedd3c3693c7081f8289860"
+
+[[package]]
+name = "trust-dns-proto"
+version = "0.18.0-alpha.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2a7f3a2ab8a919f5eca52a468866a67ed7d3efa265d48a652a9a3452272b413f"
+dependencies = [
+ "async-trait",
+ "enum-as-inner",
+ "failure",
+ "futures",
+ "idna",
+ "lazy_static",
+ "log",
+ "rand",
+ "smallvec",
+ "socket2",
+ "tokio",
+ "url",
+]
+
+[[package]]
+name = "trust-dns-resolver"
+version = "0.18.0-alpha.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6f90b1502b226f8b2514c6d5b37bafa8c200d7ca4102d57dc36ee0f3b7a04a2f"
+dependencies = [
+ "cfg-if",
+ "failure",
+ "futures",
+ "ipconfig",
+ "lazy_static",
+ "log",
+ "lru-cache",
+ "resolv-conf",
+ "smallvec",
+ "tokio",
+ "trust-dns-proto",
+]
+
+[[package]]
+name = "try-lock"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e604eb7b43c06650e854be16a2a03155743d3752dd1c943f6829e26b7a36e382"
+
+[[package]]
+name = "twoway"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6b40075910de3a912adbd80b5d8bad6ad10a23eeb1f5bf9d4006839e899ba5bc"
+dependencies = [
+ "memchr",
+ "unchecked-index",
+]
+
+[[package]]
+name = "typenum"
+version = "1.12.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "373c8a200f9e67a0c95e62a4f52fbf80c23b4381c05a17845531982fa99e6b33"
+
+[[package]]
+name = "unchecked-index"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eeba86d422ce181a719445e51872fa30f1f7413b62becb52e95ec91aa262d85c"
+
+[[package]]
+name = "unicase"
+version = "2.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "50f37be617794602aabbeee0be4f259dc1778fabe05e2d67ee8f79326d5cb4f6"
+dependencies = [
+ "version_check 0.9.2",
+]
+
+[[package]]
+name = "unicode-bidi"
+version = "0.3.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "49f2bd0c6468a8230e1db229cff8029217cf623c767ea5d60bfbd42729ea54d5"
+dependencies = [
+ "matches",
+]
+
+[[package]]
+name = "unicode-normalization"
+version = "0.1.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6fb19cf769fa8c6a80a162df694621ebeb4dafb606470b2b2fce0be40a98a977"
+dependencies = [
+ "tinyvec",
+]
+
+[[package]]
+name = "unicode-segmentation"
+version = "1.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e83e153d1053cbb5a118eeff7fd5be06ed99153f00dbcd8ae310c5fb2b22edc0"
+
+[[package]]
+name = "unicode-xid"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f7fe0bb3479651439c9112f72b6c505038574c9fbb575ed1bf3b797fa39dd564"
+
+[[package]]
+name = "url"
+version = "2.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "829d4a8476c35c9bf0bbce5a3b23f4106f79728039b726d292bb93bc106787cb"
+dependencies = [
+ "idna",
+ "matches",
+ "percent-encoding",
+]
+
+[[package]]
+name = "urlencoding"
+version = "1.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c9232eb53352b4442e40d7900465dfc534e8cb2dc8f18656fcb2ac16112b5593"
+
+[[package]]
+name = "v_escape"
+version = "0.7.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "660b101c07b5d0863deb9e7fb3138777e858d6d2a79f9e6049a27d1cc77c6da6"
+dependencies = [
+ "v_escape_derive",
+]
+
+[[package]]
+name = "v_escape_derive"
+version = "0.5.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c2ca2a14bc3fc5b64d188b087a7d3a927df87b152e941ccfbc66672e20c467ae"
+dependencies = [
+ "nom",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "v_htmlescape"
+version = "0.4.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e33e939c0d8cf047514fb6ba7d5aac78bc56677a6938b2ee67000b91f2e97e41"
+dependencies = [
+ "cfg-if",
+ "v_escape",
+]
+
+[[package]]
+name = "vcpkg"
+version = "0.2.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6454029bf181f092ad1b853286f23e2c507d8e8194d01d92da4a55c274a5508c"
+
+[[package]]
+name = "version_check"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "914b1a6776c4c929a602fafd8bc742e06365d4bcbe48c30f9cca5824f70dc9dd"
+
+[[package]]
+name = "version_check"
+version = "0.9.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b5a972e5669d67ba988ce3dc826706fb0a8b01471c088cb0b6110b805cc36aed"
+
+[[package]]
+name = "want"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1ce8a968cb1cd110d136ff8b819a556d6fb6d919363c61534f6860c7eb172ba0"
+dependencies = [
+ "log",
+ "try-lock",
+]
+
+[[package]]
+name = "wasi"
+version = "0.9.0+wasi-snapshot-preview1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519"
+
+[[package]]
+name = "wasm-bindgen"
+version = "0.2.63"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4c2dc4aa152834bc334f506c1a06b866416a8b6697d5c9f75b9a689c8486def0"
+dependencies = [
+ "cfg-if",
+ "serde",
+ "serde_json",
+ "wasm-bindgen-macro",
+]
+
+[[package]]
+name = "wasm-bindgen-backend"
+version = "0.2.63"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ded84f06e0ed21499f6184df0e0cb3494727b0c5da89534e0fcc55c51d812101"
+dependencies = [
+ "bumpalo",
+ "lazy_static",
+ "log",
+ "proc-macro2",
+ "quote",
+ "syn",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-futures"
+version = "0.4.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "64487204d863f109eb77e8462189d111f27cb5712cc9fdb3461297a76963a2f6"
+dependencies = [
+ "cfg-if",
+ "js-sys",
+ "wasm-bindgen",
+ "web-sys",
+]
+
+[[package]]
+name = "wasm-bindgen-macro"
+version = "0.2.63"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "838e423688dac18d73e31edce74ddfac468e37b1506ad163ffaf0a46f703ffe3"
+dependencies = [
+ "quote",
+ "wasm-bindgen-macro-support",
+]
+
+[[package]]
+name = "wasm-bindgen-macro-support"
+version = "0.2.63"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3156052d8ec77142051a533cdd686cba889537b213f948cd1d20869926e68e92"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+ "wasm-bindgen-backend",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-shared"
+version = "0.2.63"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c9ba19973a58daf4db6f352eda73dc0e289493cd29fb2632eb172085b6521acd"
+
+[[package]]
+name = "web-sys"
+version = "0.3.40"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7b72fe77fd39e4bd3eaa4412fd299a0be6b3dfe9d2597e2f1c20beb968f41d17"
+dependencies = [
+ "js-sys",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "whoami"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7884773ab69074615cb8f8425d0e53f11710786158704fca70f53e71b0e05504"
+
+[[package]]
+name = "widestring"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a763e303c0e0f23b0da40888724762e802a8ffefbc22de4127ef42493c2ea68c"
+
+[[package]]
+name = "winapi"
+version = "0.2.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "167dc9d6949a9b857f3451275e911c3f44255842c1f7a76f33c55103a909087a"
+
+[[package]]
+name = "winapi"
+version = "0.3.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8093091eeb260906a183e6ae1abdba2ef5ef2257a21801128899c3fc699229c6"
+dependencies = [
+ "winapi-i686-pc-windows-gnu",
+ "winapi-x86_64-pc-windows-gnu",
+]
+
+[[package]]
+name = "winapi-build"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2d315eee3b34aca4797b2da6b13ed88266e6d612562a0c46390af8299fc699bc"
+
+[[package]]
+name = "winapi-i686-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
+
+[[package]]
+name = "winapi-util"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178"
+dependencies = [
+ "winapi 0.3.8",
+]
+
+[[package]]
+name = "winapi-x86_64-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
+
+[[package]]
+name = "winreg"
+version = "0.6.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b2986deb581c4fe11b621998a5e53361efe6b48a151178d0cd9eeffa4dc6acc9"
+dependencies = [
+ "winapi 0.3.8",
+]
+
+[[package]]
+name = "winreg"
+version = "0.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0120db82e8a1e0b9fb3345a539c478767c0048d842860994d96113d5b667bd69"
+dependencies = [
+ "winapi 0.3.8",
+]
+
+[[package]]
+name = "ws2_32-sys"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d59cefebd0c892fa2dd6de581e937301d8552cb44489cdff035c6187cb63fa5e"
+dependencies = [
+ "winapi 0.2.8",
+ "winapi-build",
+]
diff --git a/Cargo.toml b/Cargo.toml
index fd495f1aa..145f7e88f 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,10 +1,14 @@
[package]
-name = "modrinth"
+name = "labrinth"
version = "0.1.0"
#Team members, please add your emails and usernames
-authors = ["geometrically ", "Redblueflame ", "Aeledfyr ", "cfanoulis"]
+authors = ["geometrically ", "Redblueflame ", "Aeledfyr ", "Charalampos Fanoulis "]
edition = "2018"
+[[bin]]
+name = "labrinth"
+path = "src/main.rs"
+
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
@@ -18,7 +22,7 @@ reqwest = {version="0.10.6", features=["json"]}
meilisearch-sdk = "0.1.4"
serde_json = "1.0"
-serde = {version="1.0", features=["derive"]}
+serde = { version = "1.0", features = ["derive"] }
chrono = { version = "0.4", features = ["serde"] }
rand = "0.7"
@@ -26,9 +30,6 @@ dotenv = "0.15"
log = "0.4.8"
env_logger = "0.7.1"
-mongodb = "1.0.0"
-bson = "1.0.0"
-
thiserror = "1.0.20"
async-trait = "0.1.36"
@@ -38,6 +39,14 @@ futures-timer = "3.0.2"
base64 = "0.12.3"
sha1 = {version="0.6.0", features=["std"]}
-[features]
-default = []
-backblaze = []
+[dependencies.sqlx]
+git = "https://github.com/launchbadge/sqlx/"
+branch = "master"
+default-features = false
+features = ["runtime-actix", "postgres", "chrono", "offline", "macros"]
+
+[dependencies.sqlx-macros]
+git = "https://github.com/launchbadge/sqlx/"
+branch = "master"
+default-features = false
+features = ["runtime-actix", "postgres", "chrono", "offline"]
diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 000000000..99c4128ac
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,26 @@
+FROM rust:1.43.1 as build
+ENV PKG_CONFIG_ALLOW_CROSS=1
+
+WORKDIR /usr/src/labrinth
+# Download and compile deps
+COPY Cargo.toml .
+COPY Cargo.lock .
+COPY docker_utils/dummy.rs .
+# Change temporarely the path of the code
+RUN sed -i 's|src/main.rs|dummy.rs|' Cargo.toml
+# Build only deps
+RUN cargo build --release
+# Now return the file back to normal
+RUN sed -i 's|dummy.rs|src/main.rs|' Cargo.toml
+
+# Copy everything
+COPY . .
+# Build our code
+ARG SQLX_OFFLINE=true
+RUN cargo build --release
+
+
+FROM gcr.io/distroless/cc-debian10
+COPY --from=build /usr/src/labrinth/target/release/labrinth /usr/local/bin/labrinth
+
+CMD ["labrinth"]
diff --git a/docker-compose.yml b/docker-compose.yml
index 20e03a704..beefeb7ab 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -1,24 +1,15 @@
version: '3'
services:
- mongo:
- image: mongo
- restart: always
- ports:
- - 27017:27017
- environment:
- MONGO_INITDB_ROOT_USERNAME: toor
- MONGO_INITDB_ROOT_PASSWORD: modrinthadmin
+ postgres_db:
+ image: postgres:alpine
volumes:
- - mongodb-data:/data/db
- mongo-express:
- image: mongo-express
- restart: always
+ - db-data:/var/lib/postgresql/data
ports:
- - 8081:8081
+ - 5432:5432
environment:
- ME_CONFIG_MONGODB_ADMINUSERNAME: toor
- ME_CONFIG_MONGODB_ADMINPASSWORD: modrinthadmin
- ME_CONFIG_OPTIONS_EDITORTHEME: material
+ POSTGRES_DB: postgres
+ POSTGRES_USER: labrinth
+ POSTGRES_HOST_AUTH_METHOD: trust
meilisearch:
image: getmeili/meilisearch
restart: on-failure
@@ -26,8 +17,17 @@ services:
- 7700:7700
volumes:
- meilisearch-data:/data.ms
+ pgadmin:
+ image: dpage/pgadmin4:latest
+ environment:
+ PGADMIN_DEFAULT_EMAIL: admin@modrinth.com
+ PGADMIN_DEFAULT_PASSWORD: secret
+ PGADMIN_CONFIG_SERVER_MODE: "False"
+ PGADMIN_CONFIG_MASTER_PASSWORD_REQUIRED: "False"
+ ports:
+ - "8070:80"
+ volumes:
+ - ./pgadmin_default_servers.json:/pgadmin4/servers.json
volumes:
meilisearch-data:
- external: true
- mongodb-data:
- external: true
\ No newline at end of file
+ db-data:
diff --git a/docker_utils/dummy.rs b/docker_utils/dummy.rs
new file mode 100644
index 000000000..e71fdf554
--- /dev/null
+++ b/docker_utils/dummy.rs
@@ -0,0 +1 @@
+fn main() {}
\ No newline at end of file
diff --git a/migrations/20200716160921_init.sql b/migrations/20200716160921_init.sql
new file mode 100644
index 000000000..a53fc3901
--- /dev/null
+++ b/migrations/20200716160921_init.sql
@@ -0,0 +1,102 @@
+CREATE TABLE users (
+ -- TODO
+ id bigint PRIMARY KEY
+);
+
+CREATE TABLE game_versions (
+ id serial PRIMARY KEY,
+ version varchar(255) NOT NULL
+);
+
+CREATE TABLE loaders (
+ id serial PRIMARY KEY,
+ loader varchar(255) NOT NULL
+);
+
+CREATE TABLE teams (
+ id bigint PRIMARY KEY
+);
+
+CREATE TABLE release_channel (
+ id serial PRIMARY KEY,
+ channel varchar(255)
+);
+
+CREATE TABLE mods (
+ id bigint PRIMARY KEY,
+ team_id bigint REFERENCES teams NOT NULL,
+ title varchar(255) NOT NULL,
+ description varchar(2048) NOT NULL,
+ body_url varchar(2048) NOT NULL,
+ published timestamptz DEFAULT CURRENT_TIMESTAMP NOT NULL,
+ downloads integer NOT NULL DEFAULT 0,
+
+ icon_url varchar(2048) NULL,
+ issues_url varchar(2048) NULL,
+ source_url varchar(2048) NULL,
+ wiki_url varchar(2048) NULL
+);
+
+
+CREATE TABLE versions (
+ id bigint PRIMARY KEY,
+ mod_id bigint REFERENCES mods,
+ name varchar(255) NOT NULL,
+ version_number varchar(255) NOT NULL,
+ changelog_url varchar(255) NULL,
+ date_published timestamptz DEFAULT CURRENT_TIMESTAMP NOT NULL,
+ downloads integer NOT NULL DEFAULT 0,
+
+ release_channel int REFERENCES release_channel ON UPDATE CASCADE NOT NULL
+);
+
+CREATE TABLE loaders_versions (
+ loader_id int REFERENCES loaders ON UPDATE CASCADE NOT NULL,
+ version_id bigint REFERENCES versions ON UPDATE CASCADE NOT NULL,
+ PRIMARY KEY (loader_id, version_id)
+);
+
+CREATE TABLE game_versions_versions (
+ game_version_id integer REFERENCES game_versions ON UPDATE CASCADE NOT NULL,
+ joining_version_id bigint REFERENCES versions ON UPDATE CASCADE NOT NULL,
+ PRIMARY KEY (game_version_id, joining_version_id)
+);
+
+CREATE TABLE files (
+ id bigint PRIMARY KEY,
+ version_id bigint REFERENCES versions NOT NULL,
+ url varchar(2048) NOT NULL
+);
+
+CREATE TABLE hashes (
+ file_id bigint REFERENCES files NOT NULL,
+ algorithm varchar(255) NOT NULL,
+ hash bytea NOT NULL,
+ PRIMARY KEY (file_id, algorithm)
+);
+
+CREATE TABLE dependencies (
+ id serial PRIMARY KEY,
+ dependent_id bigint REFERENCES versions ON UPDATE CASCADE NOT NULL,
+ dependency_id bigint REFERENCES versions ON UPDATE CASCADE NOT NULL,
+ CONSTRAINT valid_dependency CHECK (dependent_id <> dependency_id) -- No dependency on yourself
+);
+
+CREATE TABLE team_members (
+ id bigint PRIMARY KEY,
+ team_id bigint REFERENCES teams NOT NULL,
+ user_id bigint REFERENCES users NOT NULL,
+ member_name varchar(255) NOT NULL,
+ role varchar(255) NOT NULL
+);
+
+CREATE TABLE categories (
+ id serial PRIMARY KEY,
+ category varchar(255) UNIQUE
+);
+
+CREATE TABLE mods_categories (
+ joining_mod_id bigint REFERENCES mods ON UPDATE CASCADE NOT NULL,
+ joining_category_id int REFERENCES categories ON UPDATE CASCADE NOT NULL,
+ PRIMARY KEY (joining_mod_id, joining_category_id)
+);
diff --git a/migrations/20200717192808_Make_categories_non-null.sql b/migrations/20200717192808_Make_categories_non-null.sql
new file mode 100644
index 000000000..d4edc4c78
--- /dev/null
+++ b/migrations/20200717192808_Make_categories_non-null.sql
@@ -0,0 +1,3 @@
+-- Add migration script here
+ALTER TABLE categories
+ALTER COLUMN category SET NOT NULL;
diff --git a/migrations/20200722031742_initial-release-channels.sql b/migrations/20200722031742_initial-release-channels.sql
new file mode 100644
index 000000000..ba42ad5c4
--- /dev/null
+++ b/migrations/20200722031742_initial-release-channels.sql
@@ -0,0 +1,7 @@
+-- Add migration script here
+INSERT INTO release_channel (channel) VALUES ('release');
+INSERT INTO release_channel (channel) VALUES ('release-hidden');
+INSERT INTO release_channel (channel) VALUES ('beta');
+INSERT INTO release_channel (channel) VALUES ('beta-hidden');
+INSERT INTO release_channel (channel) VALUES ('alpha');
+INSERT INTO release_channel (channel) VALUES ('alpha-hidden');
\ No newline at end of file
diff --git a/migrations/20200722033157_rename-release-channels.sql b/migrations/20200722033157_rename-release-channels.sql
new file mode 100644
index 000000000..4a19ebf8c
--- /dev/null
+++ b/migrations/20200722033157_rename-release-channels.sql
@@ -0,0 +1,2 @@
+-- Add migration script here
+ALTER TABLE release_channel RENAME TO release_channels
\ No newline at end of file
diff --git a/migrations/20200722153930_version-filename.sql b/migrations/20200722153930_version-filename.sql
new file mode 100644
index 000000000..1ba726439
--- /dev/null
+++ b/migrations/20200722153930_version-filename.sql
@@ -0,0 +1,3 @@
+-- Add migration script here
+ALTER TABLE files
+ADD filename varchar(2048) NOT NULL;
diff --git a/pgadmin_default_servers.json b/pgadmin_default_servers.json
new file mode 100644
index 000000000..18e3cf624
--- /dev/null
+++ b/pgadmin_default_servers.json
@@ -0,0 +1,18 @@
+{
+ "Servers": {
+ "1": {
+ "Name": "Labrinth",
+ "Group": "Servers",
+ "Host": "postgres_db",
+ "Port": 5432,
+ "MaintenanceDB": "postgres",
+ "Username": "labrinth",
+ "SSLMode": "prefer",
+ "SSLCompression": 0,
+ "Timeout": 10,
+ "UseSSHTunnel": 0,
+ "TunnelPort": "22",
+ "TunnelAuthentication": 0
+ }
+ }
+}
diff --git a/sqlx-data.json b/sqlx-data.json
new file mode 100644
index 000000000..dc0c66b1a
--- /dev/null
+++ b/sqlx-data.json
@@ -0,0 +1,197 @@
+{
+ "db": "PostgreSQL",
+ "1ffce9b2d5c9fa6c8b9abce4bad9f9419c44ad6367b7463b979c91b9b5b4fea1": {
+ "query": "SELECT EXISTS(SELECT 1 FROM versions WHERE id=$1)",
+ "describe": {
+ "columns": [
+ {
+ "ordinal": 0,
+ "name": "exists",
+ "type_info": "Bool"
+ }
+ ],
+ "parameters": {
+ "Left": [
+ "Int8"
+ ]
+ },
+ "nullable": [
+ null
+ ]
+ }
+ },
+ "4c99c0840159d18e88cd6094a41117258f2337346c145d926b5b610c76b5125f": {
+ "query": "\n SELECT c.category\n FROM mods_categories mc\n INNER JOIN categories c ON mc.joining_category_id=c.id\n WHERE mc.joining_mod_id = $1\n ",
+ "describe": {
+ "columns": [
+ {
+ "ordinal": 0,
+ "name": "category",
+ "type_info": "Varchar"
+ }
+ ],
+ "parameters": {
+ "Left": [
+ "Int8"
+ ]
+ },
+ "nullable": [
+ false
+ ]
+ }
+ },
+ "c0899dcff4d7bc1ba3e953e5099210316bff2f98e6ab77ba84bc612eac4bce0a": {
+ "query": "\n SELECT gv.version FROM versions\n INNER JOIN game_versions_versions gvv ON gvv.joining_version_id=versions.id\n INNER JOIN game_versions gv ON gvv.game_version_id=gv.id\n WHERE versions.mod_id = $1\n ",
+ "describe": {
+ "columns": [
+ {
+ "ordinal": 0,
+ "name": "version",
+ "type_info": "Varchar"
+ }
+ ],
+ "parameters": {
+ "Left": [
+ "Int8"
+ ]
+ },
+ "nullable": [
+ false
+ ]
+ }
+ },
+ "ccd913bb2f3006ffe881ce2fc4ef1e721d18fe2eed6ac62627046c955129610c": {
+ "query": "SELECT EXISTS(SELECT 1 FROM files WHERE id=$1)",
+ "describe": {
+ "columns": [
+ {
+ "ordinal": 0,
+ "name": "exists",
+ "type_info": "Bool"
+ }
+ ],
+ "parameters": {
+ "Left": [
+ "Int8"
+ ]
+ },
+ "nullable": [
+ null
+ ]
+ }
+ },
+ "d6453e50041b5521fa9e919a9162e533bb9426f8c584d98474c6ad414db715c8": {
+ "query": "SELECT EXISTS(SELECT 1 FROM mods WHERE id=$1)",
+ "describe": {
+ "columns": [
+ {
+ "ordinal": 0,
+ "name": "exists",
+ "type_info": "Bool"
+ }
+ ],
+ "parameters": {
+ "Left": [
+ "Int8"
+ ]
+ },
+ "nullable": [
+ null
+ ]
+ }
+ },
+ "e7d0a64a08df6783c942f2fcadd94dd45f8d96ad3d3736e52ce90f68d396cdab": {
+ "query": "SELECT EXISTS(SELECT 1 FROM team_members WHERE id=$1)",
+ "describe": {
+ "columns": [
+ {
+ "ordinal": 0,
+ "name": "exists",
+ "type_info": "Bool"
+ }
+ ],
+ "parameters": {
+ "Left": [
+ "Int8"
+ ]
+ },
+ "nullable": [
+ null
+ ]
+ }
+ },
+ "e8d4589132b094df1e7a3ca0440344fc8013c0d20b3c71a1142ccbee91fb3c70": {
+ "query": "SELECT EXISTS(SELECT 1 FROM teams WHERE id=$1)",
+ "describe": {
+ "columns": [
+ {
+ "ordinal": 0,
+ "name": "exists",
+ "type_info": "Bool"
+ }
+ ],
+ "parameters": {
+ "Left": [
+ "Int8"
+ ]
+ },
+ "nullable": [
+ null
+ ]
+ }
+ },
+ "efe1bc80203f608226fa33e44654b681cc4430cec63bf7cf09b5281ff8c1c437": {
+ "query": "\n SELECT m.id, m.title, m.description, m.downloads, m.icon_url, m.body_url, m.published FROM mods m\n ",
+ "describe": {
+ "columns": [
+ {
+ "ordinal": 0,
+ "name": "id",
+ "type_info": "Int8"
+ },
+ {
+ "ordinal": 1,
+ "name": "title",
+ "type_info": "Varchar"
+ },
+ {
+ "ordinal": 2,
+ "name": "description",
+ "type_info": "Varchar"
+ },
+ {
+ "ordinal": 3,
+ "name": "downloads",
+ "type_info": "Int4"
+ },
+ {
+ "ordinal": 4,
+ "name": "icon_url",
+ "type_info": "Varchar"
+ },
+ {
+ "ordinal": 5,
+ "name": "body_url",
+ "type_info": "Varchar"
+ },
+ {
+ "ordinal": 6,
+ "name": "published",
+ "type_info": "Timestamptz"
+ }
+ ],
+ "parameters": {
+ "Left": []
+ },
+ "nullable": [
+ false,
+ false,
+ false,
+ false,
+ true,
+ false,
+ false
+ ]
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/database/mod.rs b/src/database/mod.rs
index 421661466..b6e0ea82e 100644
--- a/src/database/mod.rs
+++ b/src/database/mod.rs
@@ -1,19 +1,6 @@
pub mod models;
-mod mongo_database;
+mod postgres_database;
pub use models::Mod;
pub use models::Version;
-pub use mongo_database::connect;
-use thiserror::Error;
-
-type Result = std::result::Result;
-
-#[derive(Error, Debug)]
-pub enum DatabaseError {
- #[error("Impossible to find document")]
- NotFound(),
- #[error("BSON deserialization error")]
- BsonError(#[from] bson::de::Error),
- #[error("Local database error")]
- LocalDatabaseError(#[from] mongodb::error::Error),
-}
+pub use postgres_database::connect;
diff --git a/src/database/models/ids.rs b/src/database/models/ids.rs
new file mode 100644
index 000000000..2ae5d40c5
--- /dev/null
+++ b/src/database/models/ids.rs
@@ -0,0 +1,141 @@
+use super::DatabaseError;
+use crate::models::ids::random_base62;
+use sqlx_macros::Type;
+
+const ID_RETRY_COUNT: usize = 20;
+
+macro_rules! generate_ids {
+ ($vis:vis $function_name:ident, $return_type:ty, $id_length:expr, $select_stmnt:literal, $id_function:expr) => {
+ $vis async fn $function_name(
+ con: &mut sqlx::Transaction<'_, sqlx::Postgres>,
+ ) -> Result<$return_type, DatabaseError> {
+ let length = $id_length;
+ let mut id = random_base62(length);
+ let mut retry_count = 0;
+
+ // Check if ID is unique
+ loop {
+ let results = sqlx::query!($select_stmnt, id as i64)
+ .fetch_one(&mut *con)
+ .await?;
+
+ if results.exists.unwrap_or(true) {
+ id = random_base62(length);
+ } else {
+ break;
+ }
+
+ retry_count += 1;
+ if retry_count > ID_RETRY_COUNT {
+ return Err(DatabaseError::RandomIdError);
+ }
+ }
+
+ Ok($id_function(id as i64))
+ }
+ };
+}
+
+generate_ids!(
+ pub generate_mod_id,
+ ModId,
+ 8,
+ "SELECT EXISTS(SELECT 1 FROM mods WHERE id=$1)",
+ ModId
+);
+generate_ids!(
+ pub generate_version_id,
+ VersionId,
+ 8,
+ "SELECT EXISTS(SELECT 1 FROM versions WHERE id=$1)",
+ VersionId
+);
+generate_ids!(
+ pub generate_team_id,
+ TeamId,
+ 8,
+ "SELECT EXISTS(SELECT 1 FROM teams WHERE id=$1)",
+ TeamId
+);
+generate_ids!(
+ pub generate_file_id,
+ FileId,
+ 8,
+ "SELECT EXISTS(SELECT 1 FROM files WHERE id=$1)",
+ FileId
+);
+generate_ids!(
+ pub generate_team_member_id,
+ TeamMemberId,
+ 8,
+ "SELECT EXISTS(SELECT 1 FROM team_members WHERE id=$1)",
+ TeamMemberId
+);
+
+#[derive(Copy, Clone, Debug, Type)]
+pub struct UserId(pub i64);
+
+#[derive(Copy, Clone, Debug, Type)]
+pub struct TeamId(pub i64);
+#[derive(Copy, Clone, Debug, Type)]
+pub struct TeamMemberId(pub i64);
+
+#[derive(Copy, Clone, Debug, Type)]
+pub struct ModId(pub i64);
+
+#[derive(Copy, Clone, Debug, Type)]
+pub struct VersionId(pub i64);
+#[derive(Copy, Clone, Debug, Type)]
+pub struct ChannelId(pub i64);
+#[derive(Copy, Clone, Debug, Type)]
+pub struct GameVersionId(pub i32);
+#[derive(Copy, Clone, Debug, Type)]
+pub struct LoaderId(pub i32);
+#[derive(Copy, Clone, Debug, Type)]
+pub struct CategoryId(pub i32);
+
+#[derive(Copy, Clone, Debug, Type)]
+pub struct FileId(pub i64);
+
+use crate::models::ids;
+
+impl From for ModId {
+ fn from(id: ids::ModId) -> Self {
+ ModId(id.0 as i64)
+ }
+}
+impl From for ids::ModId {
+ fn from(id: ModId) -> Self {
+ ids::ModId(id.0 as u64)
+ }
+}
+impl From for UserId {
+ fn from(id: ids::UserId) -> Self {
+ UserId(id.0 as i64)
+ }
+}
+impl From for ids::UserId {
+ fn from(id: UserId) -> Self {
+ ids::UserId(id.0 as u64)
+ }
+}
+impl From for TeamId {
+ fn from(id: ids::TeamId) -> Self {
+ TeamId(id.0 as i64)
+ }
+}
+impl From for ids::TeamId {
+ fn from(id: TeamId) -> Self {
+ ids::TeamId(id.0 as u64)
+ }
+}
+impl From for VersionId {
+ fn from(id: ids::VersionId) -> Self {
+ VersionId(id.0 as i64)
+ }
+}
+impl From for ids::VersionId {
+ fn from(id: VersionId) -> Self {
+ ids::VersionId(id.0 as u64)
+ }
+}
diff --git a/src/database/models/mod.rs b/src/database/models/mod.rs
index 806f99f1c..758b7e821 100644
--- a/src/database/models/mod.rs
+++ b/src/database/models/mod.rs
@@ -1,32 +1,25 @@
-mod mod_item;
-mod team_item;
-mod version_item;
+#![allow(unused)]
+// TODO: remove attr once routes are created
-use crate::database::DatabaseError::NotFound;
-use crate::database::Result;
-use async_trait::async_trait;
-use bson::doc;
-use bson::Document;
+use thiserror::Error;
+
+pub mod ids;
+pub mod mod_item;
+pub mod team_item;
+pub mod version_item;
+
+pub use ids::*;
pub use mod_item::Mod;
-use mongodb::Database;
pub use team_item::Team;
pub use team_item::TeamMember;
pub use version_item::FileHash;
pub use version_item::Version;
pub use version_item::VersionFile;
-#[async_trait]
-pub trait Item {
- fn get_collection() -> &'static str;
- async fn get_by_id(client: Database, id: &str) -> Result> {
- let filter = doc! { "_id": id };
- let collection = client.collection(Self::get_collection());
- let doc: Document = match collection.find_one(filter, None).await? {
- Some(e) => e,
- None => return Err(NotFound()),
- };
- let elem: Box = Self::from_doc(doc)?;
- Ok(elem)
- }
- fn from_doc(elem: Document) -> Result>;
+#[derive(Error, Debug)]
+pub enum DatabaseError {
+ #[error("Error while interacting with the database")]
+ DatabaseError(#[from] sqlx::error::Error),
+ #[error("Error while trying to generate random ID")]
+ RandomIdError,
}
diff --git a/src/database/models/mod_item.rs b/src/database/models/mod_item.rs
index 5eff048c1..d74780ae6 100644
--- a/src/database/models/mod_item.rs
+++ b/src/database/models/mod_item.rs
@@ -1,36 +1,108 @@
-use crate::database::models::team_item::Team;
-use crate::database::models::Item;
-use crate::database::Result;
-use bson::{Bson, Document};
-use serde::{Deserialize, Serialize};
+use super::ids::*;
-#[derive(Deserialize, Serialize)]
-pub struct Mod {
- /// The ID for the mod, must be serializable to base62
- pub id: i32,
- //Todo: Move to own table
- /// The team that owns the mod
- pub team: Team,
+pub struct ModBuilder {
+ pub mod_id: ModId,
+ pub team_id: TeamId,
pub title: String,
pub description: String,
pub body_url: String,
- pub published: String,
+ pub icon_url: Option,
+ pub issues_url: Option,
+ pub source_url: Option,
+ pub wiki_url: Option,
+ pub categories: Vec,
+ pub initial_versions: Vec,
+}
+
+impl ModBuilder {
+ pub async fn insert(
+ self,
+ transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
+ ) -> Result {
+ let mod_struct = Mod {
+ id: self.mod_id,
+ team_id: self.team_id,
+ title: self.title,
+ description: self.description,
+ body_url: self.body_url,
+ published: chrono::Utc::now(),
+ downloads: 0,
+ icon_url: self.icon_url,
+ issues_url: self.issues_url,
+ source_url: self.source_url,
+ wiki_url: self.wiki_url,
+ };
+ mod_struct.insert(&mut *transaction).await?;
+
+ for mut version in self.initial_versions {
+ version.mod_id = self.mod_id;
+ version.insert(&mut *transaction).await?;
+ }
+
+ for category in self.categories {
+ sqlx::query(
+ "
+ INSERT INTO mod_categories (joining_mod_id, joining_category_id)
+ VALUES ($1, $2)
+ ",
+ )
+ .bind(self.mod_id)
+ .bind(category)
+ .execute(&mut *transaction)
+ .await?;
+ }
+
+ Ok(self.mod_id)
+ }
+}
+
+pub struct Mod {
+ pub id: ModId,
+ pub team_id: TeamId,
+ pub title: String,
+ pub description: String,
+ pub body_url: String,
+ pub published: chrono::DateTime,
pub downloads: i32,
- pub categories: Vec,
- ///A vector of Version IDs specifying the mod version of a dependency
- pub version_ids: Vec,
pub icon_url: Option,
pub issues_url: Option,
pub source_url: Option,
pub wiki_url: Option,
}
-impl Item for Mod {
- fn get_collection() -> &'static str {
- "mods"
- }
- fn from_doc(elem: Document) -> Result> {
- let result: Mod = bson::from_bson(Bson::from(elem))?;
- Ok(Box::from(result))
+impl Mod {
+ pub async fn insert(
+ &self,
+ transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
+ ) -> Result<(), sqlx::error::Error> {
+ sqlx::query(
+ "
+ INSERT INTO mods (
+ id, team_id, title, description, body_url,
+ published, downloads, icon_url, issues_url,
+ source_url, wiki_url
+ )
+ VALUES (
+ $1, $2, $3, $4, $5,
+ $6, $7, $8, $9,
+ $10, $11
+ )
+ ",
+ )
+ .bind(self.id)
+ .bind(self.team_id)
+ .bind(&self.title)
+ .bind(&self.description)
+ .bind(&self.body_url)
+ .bind(self.published)
+ .bind(self.downloads)
+ .bind(self.icon_url.as_ref())
+ .bind(self.issues_url.as_ref())
+ .bind(self.source_url.as_ref())
+ .bind(self.wiki_url.as_ref())
+ .execute(&mut *transaction)
+ .await?;
+
+ Ok(())
}
}
diff --git a/src/database/models/team_item.rs b/src/database/models/team_item.rs
index dc1b77739..2b252cd82 100644
--- a/src/database/models/team_item.rs
+++ b/src/database/models/team_item.rs
@@ -1,19 +1,74 @@
-use serde::{Deserialize, Serialize};
+use super::ids::*;
+
+pub struct TeamBuilder {
+ pub members: Vec,
+}
+pub struct TeamMemberBuilder {
+ pub user_id: UserId,
+ pub name: String,
+ pub role: String,
+}
+
+impl TeamBuilder {
+ pub async fn insert(
+ self,
+ transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
+ ) -> Result {
+ let team_id = generate_team_id(&mut *transaction).await?;
+
+ let team = Team { id: team_id };
+
+ sqlx::query(
+ "
+ INSERT INTO teams (id)
+ VALUES ($1)
+ ",
+ )
+ .bind(team.id)
+ .execute(&mut *transaction)
+ .await?;
+
+ for member in self.members {
+ let team_member_id = generate_team_member_id(&mut *transaction).await?;
+ let team_member = TeamMember {
+ id: team_member_id,
+ team_id,
+ user_id: member.user_id,
+ name: member.name,
+ role: member.role,
+ };
+
+ sqlx::query(
+ "
+ INSERT INTO team_members (id, team_id, user_id, name, role)
+ VALUES ($1, $2)
+ ",
+ )
+ .bind(team_member.id)
+ .bind(team_member.team_id)
+ .bind(team_member.user_id)
+ .bind(team_member.name)
+ .bind(team_member.role)
+ .execute(&mut *transaction)
+ .await?;
+ }
+
+ Ok(team_id)
+ }
+}
/// A team of users who control a mod
-#[derive(Serialize, Deserialize)]
pub struct Team {
/// The id of the team
- pub id: i32,
- /// A list of the members of the team
- pub members: Vec,
+ pub id: TeamId,
}
/// A member of a team
-#[derive(Serialize, Deserialize, Clone)]
pub struct TeamMember {
+ pub id: TeamMemberId,
+ pub team_id: TeamId,
/// The ID of the user associated with the member
- pub user_id: i32,
+ pub user_id: UserId,
/// The name of the user
pub name: String,
pub role: String,
diff --git a/src/database/models/version_item.rs b/src/database/models/version_item.rs
index 4521cf033..437024e14 100644
--- a/src/database/models/version_item.rs
+++ b/src/database/models/version_item.rs
@@ -1,48 +1,215 @@
-use crate::database::models::Item;
-use crate::database::Result;
-use bson::{Bson, Document};
-use serde::{Deserialize, Serialize};
+use super::ids::*;
+use super::DatabaseError;
-//TODO: Files should probably be moved to their own table
-#[derive(Deserialize, Serialize)]
-pub struct Version {
- ///The unqiue VersionId of this version
- pub version_id: i32,
- /// The ModId of the mod that this version belongs to
- pub mod_id: i32,
+pub struct VersionBuilder {
+ pub version_id: VersionId,
+ pub mod_id: ModId,
pub name: String,
- pub number: String,
+ pub version_number: String,
pub changelog_url: Option,
- pub date_published: String,
- pub downloads: i32,
- pub files: Vec,
- pub dependencies: Vec,
- pub game_versions: Vec,
- pub loaders: Vec,
- pub version_type: String,
+ pub files: Vec,
+ pub dependencies: Vec,
+ pub game_versions: Vec,
+ pub loaders: Vec,
+ pub release_channel: ChannelId,
}
-#[derive(Serialize, Deserialize)]
-pub struct VersionFile {
- pub game_versions: Vec,
- pub hashes: Vec,
+pub struct VersionFileBuilder {
pub url: String,
+ pub filename: String,
+ pub hashes: Vec,
}
-/// A hash of a mod's file
-#[derive(Serialize, Deserialize)]
-pub struct FileHash {
+pub struct HashBuilder {
pub algorithm: String,
- pub hash: String,
+ pub hash: Vec,
}
-impl Item for Version {
- fn get_collection() -> &'static str {
- "versions"
- }
+impl VersionBuilder {
+ pub async fn insert(
+ self,
+ transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
+ ) -> Result {
+ let version = Version {
+ id: self.version_id,
+ mod_id: self.mod_id,
+ name: self.name,
+ version_number: self.version_number,
+ changelog_url: self.changelog_url,
+ date_published: chrono::Utc::now(),
+ downloads: 0,
+ release_channel: self.release_channel,
+ };
- fn from_doc(elem: Document) -> Result> {
- let version: Version = bson::from_bson(Bson::from(elem))?;
- Ok(Box::from(version))
+ version.insert(&mut *transaction).await?;
+
+ for file in self.files {
+ let file_id = generate_file_id(&mut *transaction).await?;
+ sqlx::query(
+ "
+ INSERT INTO files (id, version_id, url, filename)
+ VALUES ($1, $2, $3, $4)
+ ",
+ )
+ .bind(file_id)
+ .bind(self.version_id)
+ .bind(file.url)
+ .bind(file.filename)
+ .execute(&mut *transaction)
+ .await?;
+
+ for hash in file.hashes {
+ sqlx::query(
+ "
+ INSERT INTO hashes (file_id, algorithm, hash)
+ VALUES ($1, $2, $3)
+ ",
+ )
+ .bind(file_id)
+ .bind(hash.algorithm)
+ .bind(hash.hash)
+ .execute(&mut *transaction)
+ .await?;
+ }
+ }
+
+ for dependency in self.dependencies {
+ sqlx::query(
+ "
+ INSERT INTO dependencies (dependent_id, dependency_id)
+ VALUES ($1, $2)
+ ",
+ )
+ .bind(self.version_id)
+ .bind(dependency)
+ .execute(&mut *transaction)
+ .await?;
+ }
+
+ for loader in self.loaders {
+ sqlx::query(
+ "
+ INSERT INTO dependencies (loader_id, version_id)
+ VALUES ($1, $2)
+ ",
+ )
+ .bind(loader)
+ .bind(self.version_id)
+ .execute(&mut *transaction)
+ .await?;
+ }
+
+ for game_version in self.game_versions {
+ sqlx::query(
+ "
+ INSERT INTO dependencies (game_version_id, joining_version_id)
+ VALUES ($1, $2)
+ ",
+ )
+ .bind(game_version)
+ .bind(self.version_id)
+ .execute(&mut *transaction)
+ .await?;
+ }
+
+ Ok(self.version_id)
}
}
+
+pub struct Version {
+ pub id: VersionId,
+ pub mod_id: ModId,
+ pub name: String,
+ pub version_number: String,
+ pub changelog_url: Option,
+ pub date_published: chrono::DateTime,
+ pub downloads: i32,
+ pub release_channel: ChannelId,
+}
+
+impl Version {
+ pub async fn insert(
+ &self,
+ transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
+ ) -> Result<(), sqlx::error::Error> {
+ sqlx::query(
+ "
+ INSERT INTO versions (
+ id, mod_id, name, version_number,
+ changelog_url, date_published,
+ downloads, release_channel
+ )
+ VALUES (
+ $1, $2, $3, $4,
+ $5, $6,
+ $7, $8
+ )
+ ",
+ )
+ .bind(self.id)
+ .bind(self.mod_id)
+ .bind(&self.name)
+ .bind(&self.version_number)
+ .bind(self.changelog_url.as_ref())
+ .bind(self.date_published)
+ .bind(self.downloads)
+ .bind(self.release_channel)
+ .execute(&mut *transaction)
+ .await?;
+
+ Ok(())
+ }
+
+ pub async fn get_dependencies<'a, E>(&self, exec: E) -> Result, sqlx::Error>
+ where
+ E: sqlx::Executor<'a, Database = sqlx::Postgres>,
+ {
+ use futures::stream::TryStreamExt;
+
+ let vec = sqlx::query_as::<_, (VersionId,)>(
+ "
+ SELECT id FROM versions v
+ INNER JOIN dependencies d ON d.dependency_id = v.id
+ WHERE d.dependent_id = $1
+ ",
+ )
+ .bind(self.id)
+ .fetch_many(exec)
+ .try_filter_map(|e| async { Ok(e.right().map(|(v,)| v)) })
+ .try_collect::>()
+ .await?;
+
+ Ok(vec)
+ }
+}
+
+pub struct ReleaseChannel {
+ pub id: ChannelId,
+ pub channel: String,
+}
+pub struct Loader {
+ pub id: LoaderId,
+ pub loader: String,
+}
+pub struct GameVersion {
+ pub id: GameVersionId,
+ pub version: String,
+}
+
+pub struct VersionFile {
+ pub id: FileId,
+ pub version_id: VersionId,
+ pub url: String,
+ pub filename: String,
+}
+
+pub struct FileHash {
+ pub file_id: FileId,
+ pub algorithm: String,
+ pub hash: Vec,
+}
+
+pub struct Category {
+ pub id: CategoryId,
+ pub category: String,
+}
diff --git a/src/database/mongo_database.rs b/src/database/mongo_database.rs
deleted file mode 100644
index d4545975a..000000000
--- a/src/database/mongo_database.rs
+++ /dev/null
@@ -1,14 +0,0 @@
-use log::info;
-use mongodb::error::Error;
-use mongodb::options::ClientOptions;
-use mongodb::Client;
-
-pub async fn connect() -> Result {
- info!("Initializing database connection");
-
- let mongodb_addr = dotenv::var("MONGODB_ADDR").expect("`MONGO_ADDR` not in .env");
- let mut client_options = ClientOptions::parse(&mongodb_addr).await?;
- client_options.app_name = Some("labrinth".to_string());
-
- Client::with_options(client_options)
-}
diff --git a/src/database/postgres_database.rs b/src/database/postgres_database.rs
new file mode 100644
index 000000000..634cc80b9
--- /dev/null
+++ b/src/database/postgres_database.rs
@@ -0,0 +1,14 @@
+use log::info;
+use sqlx::postgres::{PgPool, PgPoolOptions};
+
+pub async fn connect() -> Result {
+ info!("Initializing database connection");
+
+ let database_url = dotenv::var("DATABASE_URL").expect("`DATABASE_URL` not in .env");
+ let pool = PgPoolOptions::new()
+ .max_connections(20)
+ .connect(&database_url)
+ .await?;
+
+ Ok(pool)
+}
diff --git a/src/file_hosting/backblaze.rs b/src/file_hosting/backblaze.rs
new file mode 100644
index 000000000..ee9f7d7be
--- /dev/null
+++ b/src/file_hosting/backblaze.rs
@@ -0,0 +1,135 @@
+use super::{DeleteFileData, FileHost, FileHostingError, UploadFileData};
+use async_trait::async_trait;
+
+mod authorization;
+mod delete;
+mod upload;
+
+pub struct BackblazeHost {
+ upload_url_data: authorization::UploadUrlData,
+ authorization_data: authorization::AuthorizationData,
+}
+
+impl BackblazeHost {
+ pub async fn new(key_id: &str, key: &str, bucket_id: &str) -> Self {
+ let authorization_data = authorization::authorize_account(key_id, key).await.unwrap();
+ let upload_url_data = authorization::get_upload_url(&authorization_data, bucket_id)
+ .await
+ .unwrap();
+
+ BackblazeHost {
+ authorization_data,
+ upload_url_data,
+ }
+ }
+}
+
+#[async_trait]
+impl FileHost for BackblazeHost {
+ async fn upload_file(
+ &self,
+ content_type: &str,
+ file_name: &str,
+ file_bytes: Vec,
+ ) -> Result {
+ let upload_data =
+ upload::upload_file(&self.upload_url_data, content_type, file_name, file_bytes).await?;
+ Ok(UploadFileData {
+ file_id: upload_data.file_id,
+ file_name: upload_data.file_name,
+ content_length: upload_data.content_length,
+ content_sha1: upload_data.content_sha1,
+ content_md5: upload_data.content_md5,
+ content_type: upload_data.content_type,
+ upload_timestamp: upload_data.upload_timestamp,
+ })
+ }
+
+ /*
+ async fn upload_file_streaming(
+ &self,
+ content_type: &str,
+ file_name: &str,
+ stream: reqwest::Body
+ ) -> Result {
+ use futures::stream::StreamExt;
+
+ let mut data = Vec::new();
+ while let Some(chunk) = stream.next().await {
+ data.extend_from_slice(&chunk.map_err(|e| FileHostingError::Other(e))?);
+ }
+ self.upload_file(content_type, file_name, data).await
+ }
+ */
+
+ async fn delete_file_version(
+ &self,
+ file_id: &str,
+ file_name: &str,
+ ) -> Result {
+ let delete_data =
+ delete::delete_file_version(&self.authorization_data, file_id, file_name).await?;
+ Ok(DeleteFileData {
+ file_id: delete_data.file_id,
+ file_name: delete_data.file_name,
+ })
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use authorization::*;
+ use delete::*;
+ use upload::*;
+
+ #[actix_rt::test]
+ async fn test_authorization() {
+ println!("{}", dotenv::var("BACKBLAZE_BUCKET_ID").unwrap());
+ let authorization_data = authorize_account(
+ &dotenv::var("BACKBLAZE_KEY_ID").unwrap(),
+ &dotenv::var("BACKBLAZE_KEY").unwrap(),
+ )
+ .await
+ .unwrap();
+
+ get_upload_url(
+ &authorization_data,
+ &dotenv::var("BACKBLAZE_BUCKET_ID").unwrap(),
+ )
+ .await
+ .unwrap();
+ }
+
+ #[actix_rt::test]
+ async fn test_file_management() {
+ let authorization_data = authorize_account(
+ &dotenv::var("BACKBLAZE_KEY_ID").unwrap(),
+ &dotenv::var("BACKBLAZE_KEY").unwrap(),
+ )
+ .await
+ .unwrap();
+ let upload_url_data = get_upload_url(
+ &authorization_data,
+ &dotenv::var("BACKBLAZE_BUCKET_ID").unwrap(),
+ )
+ .await
+ .unwrap();
+ let upload_data = upload_file(
+ &upload_url_data,
+ "text/plain",
+ "test.txt",
+ "test file".to_string().into_bytes(),
+ )
+ .await
+ .unwrap();
+
+ delete_file_version(
+ &authorization_data,
+ &upload_data.file_id,
+ &upload_data.file_name,
+ )
+ .await
+ .unwrap();
+ }
+}
diff --git a/src/file_hosting/authorization.rs b/src/file_hosting/backblaze/authorization.rs
similarity index 53%
rename from src/file_hosting/authorization.rs
rename to src/file_hosting/backblaze/authorization.rs
index eb14cad6e..eb18d17c7 100644
--- a/src/file_hosting/authorization.rs
+++ b/src/file_hosting/backblaze/authorization.rs
@@ -30,10 +30,9 @@ pub struct UploadUrlData {
pub authorization_token: String,
}
-#[cfg(feature = "backblaze")]
pub async fn authorize_account(
- key_id: String,
- application_key: String,
+ key_id: &str,
+ application_key: &str,
) -> Result {
let combined_key = format!("{}:{}", key_id, application_key);
let formatted_key = format!("Basic {}", base64::encode(combined_key));
@@ -52,17 +51,16 @@ pub async fn authorize_account(
}
}
-#[cfg(feature = "backblaze")]
pub async fn get_upload_url(
- authorization_data: AuthorizationData,
- bucket_id: String,
+ authorization_data: &AuthorizationData,
+ bucket_id: &str,
) -> Result {
let response = reqwest::Client::new()
.post(&format!("{}/b2api/v2/b2_get_upload_url", authorization_data.api_url).to_string())
.header(reqwest::header::CONTENT_TYPE, "application/json")
.header(
reqwest::header::AUTHORIZATION,
- authorization_data.authorization_token,
+ &authorization_data.authorization_token,
)
.body(
serde_json::json!({
@@ -79,50 +77,3 @@ pub async fn get_upload_url(
Err(FileHostingError::BackblazeError(response.json().await?))
}
}
-
-#[cfg(not(feature = "backblaze"))]
-pub async fn authorize_account(
- _key_id: String,
- _application_key: String,
-) -> Result {
- Ok(AuthorizationData {
- absolute_minimum_part_size: 5000000,
- account_id: String::from("MOCK_ACCOUNT_ID"),
- allowed: AuthorizationPermissions {
- bucket_id: None,
- bucket_name: None,
- capabilities: vec![
- String::from("listKeys"),
- String::from("writeKeys"),
- String::from("deleteKeys"),
- String::from("listAllBucketNames"),
- String::from("listBuckets"),
- String::from("writeBuckets"),
- String::from("deleteBuckets"),
- String::from("readBuckets"),
- String::from("listFiles"),
- String::from("readFiles"),
- String::from("shareFiles"),
- String::from("writeFiles"),
- String::from("deleteFiles"),
- ],
- name_prefix: None,
- },
- api_url: String::from("https://api.example.com"),
- authorization_token: String::from("MOCK_AUTH_TOKEN"),
- download_url: String::from("https://download.example.com"),
- recommended_part_size: 100000000,
- })
-}
-
-#[cfg(not(feature = "backblaze"))]
-pub async fn get_upload_url(
- _authorization_data: AuthorizationData,
- _bucket_id: String,
-) -> Result {
- Ok(UploadUrlData {
- bucket_id: String::from("MOCK_BUCKET_ID"),
- upload_url: String::from("https://download.example.com"),
- authorization_token: String::from("MOCK_AUTH_TOKEN"),
- })
-}
diff --git a/src/file_hosting/delete.rs b/src/file_hosting/backblaze/delete.rs
similarity index 65%
rename from src/file_hosting/delete.rs
rename to src/file_hosting/backblaze/delete.rs
index 2ea54bd2e..f7709f0c3 100644
--- a/src/file_hosting/delete.rs
+++ b/src/file_hosting/backblaze/delete.rs
@@ -1,4 +1,5 @@
-use crate::file_hosting::{AuthorizationData, FileHostingError};
+use super::authorization::AuthorizationData;
+use crate::file_hosting::FileHostingError;
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, Debug, Clone)]
@@ -8,7 +9,6 @@ pub struct DeleteFileData {
pub file_name: String,
}
-#[cfg(feature = "backblaze")]
pub async fn delete_file_version(
authorization_data: &AuthorizationData,
file_id: &str,
@@ -40,19 +40,3 @@ pub async fn delete_file_version(
Err(FileHostingError::BackblazeError(response.json().await?))
}
}
-
-#[cfg(not(feature = "backblaze"))]
-pub async fn delete_file_version(
- _authorization_data: &AuthorizationData,
- file_id: &str,
- file_name: &str,
-) -> Result {
- let path = std::path::Path::new(&dotenv::var("MOCK_FILE_PATH").unwrap())
- .join(file_name.replace("../", ""));
- std::fs::remove_file(path)?;
-
- Ok(DeleteFileData {
- file_id: file_id.to_string(),
- file_name: file_name.to_string(),
- })
-}
diff --git a/src/file_hosting/upload.rs b/src/file_hosting/backblaze/upload.rs
similarity index 57%
rename from src/file_hosting/upload.rs
rename to src/file_hosting/backblaze/upload.rs
index 355ededdb..6b4f44077 100644
--- a/src/file_hosting/upload.rs
+++ b/src/file_hosting/backblaze/upload.rs
@@ -1,4 +1,4 @@
-use crate::file_hosting::authorization::UploadUrlData;
+use super::authorization::UploadUrlData;
use crate::file_hosting::FileHostingError;
use serde::{Deserialize, Serialize};
@@ -16,7 +16,6 @@ pub struct UploadFileData {
pub upload_timestamp: u64,
}
-#[cfg(feature = "backblaze")]
//Content Types found here: https://www.backblaze.com/b2/docs/content-types.html
pub async fn upload_file(
url_data: &UploadUrlData,
@@ -47,29 +46,3 @@ pub async fn upload_file(
Err(FileHostingError::BackblazeError(response.json().await?))
}
}
-
-#[cfg(not(feature = "backblaze"))]
-pub async fn upload_file(
- _url_data: &UploadUrlData,
- content_type: &str,
- file_name: &str,
- file_bytes: Vec,
-) -> Result {
- let path = std::path::Path::new(&dotenv::var("MOCK_FILE_PATH").unwrap())
- .join(file_name.replace("../", ""));
- std::fs::create_dir_all(path.parent().ok_or(FileHostingError::InvalidFilename)?)?;
- let content_sha1 = sha1::Sha1::from(&file_bytes).hexdigest();
-
- std::fs::write(path, &file_bytes)?;
- Ok(UploadFileData {
- file_id: String::from("MOCK_FILE_ID"),
- file_name: file_name.to_string(),
- account_id: String::from("MOCK_ACCOUNT_ID"),
- bucket_id: String::from("MOCK_BUCKET_ID"),
- content_length: file_bytes.len() as u32,
- content_sha1,
- content_md5: None,
- content_type: content_type.to_string(),
- upload_timestamp: chrono::Utc::now().timestamp_millis() as u64,
- })
-}
diff --git a/src/file_hosting/mock.rs b/src/file_hosting/mock.rs
new file mode 100644
index 000000000..da14255ad
--- /dev/null
+++ b/src/file_hosting/mock.rs
@@ -0,0 +1,51 @@
+use super::{DeleteFileData, FileHost, FileHostingError, UploadFileData};
+use async_trait::async_trait;
+
+pub struct MockHost(());
+
+impl MockHost {
+ pub fn new() -> Self {
+ MockHost(())
+ }
+}
+
+#[async_trait]
+impl FileHost for MockHost {
+ async fn upload_file(
+ &self,
+ content_type: &str,
+ file_name: &str,
+ file_bytes: Vec,
+ ) -> Result {
+ let path = std::path::Path::new(&dotenv::var("MOCK_FILE_PATH").unwrap())
+ .join(file_name.replace("../", ""));
+ std::fs::create_dir_all(path.parent().ok_or(FileHostingError::InvalidFilename)?)?;
+ let content_sha1 = sha1::Sha1::from(&file_bytes).hexdigest();
+
+ std::fs::write(path, &file_bytes)?;
+ Ok(UploadFileData {
+ file_id: String::from("MOCK_FILE_ID"),
+ file_name: file_name.to_string(),
+ content_length: file_bytes.len() as u32,
+ content_sha1,
+ content_md5: None,
+ content_type: content_type.to_string(),
+ upload_timestamp: chrono::Utc::now().timestamp_millis() as u64,
+ })
+ }
+
+ async fn delete_file_version(
+ &self,
+ file_id: &str,
+ file_name: &str,
+ ) -> Result {
+ let path = std::path::Path::new(&dotenv::var("MOCK_FILE_PATH").unwrap())
+ .join(file_name.replace("../", ""));
+ std::fs::remove_file(path)?;
+
+ Ok(DeleteFileData {
+ file_id: file_id.to_string(),
+ file_name: file_name.to_string(),
+ })
+ }
+}
diff --git a/src/file_hosting/mod.rs b/src/file_hosting/mod.rs
index eca576f6d..21b416590 100644
--- a/src/file_hosting/mod.rs
+++ b/src/file_hosting/mod.rs
@@ -1,90 +1,53 @@
+use async_trait::async_trait;
use thiserror::Error;
-mod authorization;
-mod delete;
-mod upload;
+mod backblaze;
+mod mock;
-pub use authorization::authorize_account;
-pub use authorization::get_upload_url;
-pub use authorization::AuthorizationData;
-pub use authorization::AuthorizationPermissions;
-pub use authorization::UploadUrlData;
-
-pub use upload::upload_file;
-pub use upload::UploadFileData;
-
-pub use delete::delete_file_version;
-pub use delete::DeleteFileData;
+pub use backblaze::BackblazeHost;
+pub use mock::MockHost;
#[derive(Error, Debug)]
pub enum FileHostingError {
- #[cfg(feature = "backblaze")]
#[error("Error while accessing the data from backblaze")]
HttpError(#[from] reqwest::Error),
-
- #[cfg(feature = "backblaze")]
#[error("Backblaze error: {0}")]
BackblazeError(serde_json::Value),
-
- #[cfg(not(feature = "backblaze"))]
#[error("File system error in file hosting: {0}")]
FileSystemError(#[from] std::io::Error),
- #[cfg(not(feature = "backblaze"))]
#[error("Invalid Filename")]
InvalidFilename,
}
-#[cfg(test)]
-mod tests {
- use super::*;
-
- #[actix_rt::test]
- async fn test_authorization() {
- println!("{}", dotenv::var("BACKBLAZE_BUCKET_ID").unwrap());
- let authorization_data = authorize_account(
- dotenv::var("BACKBLAZE_KEY_ID").unwrap(),
- dotenv::var("BACKBLAZE_KEY").unwrap(),
- )
- .await
- .unwrap();
-
- get_upload_url(
- authorization_data,
- dotenv::var("BACKBLAZE_BUCKET_ID").unwrap(),
- )
- .await
- .unwrap();
- }
-
- #[actix_rt::test]
- async fn test_file_management() {
- let authorization_data = authorize_account(
- dotenv::var("BACKBLAZE_KEY_ID").unwrap(),
- dotenv::var("BACKBLAZE_KEY").unwrap(),
- )
- .await
- .unwrap();
- let upload_url_data = get_upload_url(
- authorization_data.clone(),
- dotenv::var("BACKBLAZE_BUCKET_ID").unwrap(),
- )
- .await
- .unwrap();
- let upload_data = upload_file(
- &upload_url_data,
- "text/plain",
- "test.txt",
- "test file".to_string().into_bytes(),
- )
- .await
- .unwrap();
-
- delete_file_version(
- &authorization_data,
- &upload_data.file_id,
- &upload_data.file_name,
- )
- .await
- .unwrap();
- }
+#[derive(Debug, Clone)]
+pub struct UploadFileData {
+ pub file_id: String,
+ pub file_name: String,
+ pub content_length: u32,
+ pub content_sha1: String,
+ pub content_md5: Option,
+ pub content_type: String,
+ pub upload_timestamp: u64,
+}
+
+#[derive(Debug, Clone)]
+pub struct DeleteFileData {
+ pub file_id: String,
+ pub file_name: String,
+}
+
+#[async_trait]
+pub trait FileHost {
+ async fn upload_file(
+ &self,
+ content_type: &str,
+ file_name: &str,
+ file_bytes: Vec,
+ ) -> Result;
+
+ async fn delete_file_version(
+ &self,
+ file_id: &str,
+ file_name: &str,
+ ) -> Result;
}
diff --git a/src/main.rs b/src/main.rs
index e66105e50..3dd648419 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -5,6 +5,7 @@ use env_logger::Env;
use log::info;
use std::env;
use std::fs::File;
+use std::sync::Arc;
mod database;
mod file_hosting;
@@ -19,54 +20,57 @@ async fn main() -> std::io::Result<()> {
check_env_vars();
- //Database Connecter
- let client = database::connect()
+ // Database Connector
+ let pool = database::connect()
.await
.expect("Database connection failed");
- let client_ref = client.clone();
+ let client_ref = pool.clone();
- //File Hosting Initializer
- let authorization_data = file_hosting::authorize_account(
- dotenv::var("BACKBLAZE_KEY_ID").unwrap(),
- dotenv::var("BACKBLAZE_KEY").unwrap(),
- )
- .await
- .unwrap();
- let upload_url_data = file_hosting::get_upload_url(
- authorization_data.clone(),
- dotenv::var("BACKBLAZE_BUCKET_ID").unwrap(),
- )
- .await
- .unwrap();
+ let backblaze_enabled = dotenv::var("BACKBLAZE_ENABLED")
+ .ok()
+ .and_then(|s| s.parse::().ok())
+ .unwrap_or(false);
+
+ let file_host: Arc = if backblaze_enabled {
+ Arc::new(
+ file_hosting::BackblazeHost::new(
+ &dotenv::var("BACKBLAZE_KEY_ID").unwrap(),
+ &dotenv::var("BACKBLAZE_KEY").unwrap(),
+ &dotenv::var("BACKBLAZE_BUCKET_ID").unwrap(),
+ )
+ .await,
+ )
+ } else {
+ Arc::new(file_hosting::MockHost::new())
+ };
// Get executable path
let mut exe_path = env::current_exe()?.parent().unwrap().to_path_buf();
// Create the path to the index lock file
exe_path.push("index.v1.lock");
- //Indexing mods if not already done
+ // Indexing mods if not already done
if env::args().any(|x| x == "regen") {
// User forced regen of indexing
info!("Forced regeneration of indexes!");
- index_mods(client).await.expect("Mod indexing failed");
+ index_mods(pool).await.expect("Mod indexing failed");
} else if !exe_path.exists() {
// The indexes were not created, or the version was upgraded
info!("Indexing of mods for first time...");
- index_mods(client).await.expect("Mod indexing failed");
+ index_mods(pool).await.expect("Mod indexing failed");
// Create the lock file
File::create(exe_path)?;
}
info!("Starting Actix HTTP server!");
- //Init App
+ // Init App
HttpServer::new(move || {
App::new()
.wrap(Logger::default())
.wrap(Logger::new("%a %{User-Agent}i"))
.data(client_ref.clone())
- .data(authorization_data.clone())
- .data(upload_url_data.clone())
+ .data(file_host.clone())
.service(routes::index_get)
.service(routes::mod_search)
.service(routes::mod_create)
@@ -93,11 +97,17 @@ fn check_env_vars() {
}
}
check_var::("INDEX_CURSEFORGE");
- check_var::("MONGODB_ADDR");
+ check_var::("DATABASE_URL");
check_var::("MEILISEARCH_ADDR");
check_var::("BIND_ADDR");
- check_var::("BACKBLAZE_KEY_ID");
- check_var::("BACKBLAZE_KEY");
- check_var::("BACKBLAZE_BUCKET_ID");
+ if dotenv::var("BACKBLAZE_ENABLED")
+ .ok()
+ .and_then(|s| s.parse::().ok())
+ .unwrap_or(false)
+ {
+ check_var::("BACKBLAZE_KEY_ID");
+ check_var::("BACKBLAZE_KEY");
+ check_var::("BACKBLAZE_BUCKET_ID");
+ }
}
diff --git a/src/models/mods.rs b/src/models/mods.rs
index 735f767c1..7df7bbdb3 100644
--- a/src/models/mods.rs
+++ b/src/models/mods.rs
@@ -100,6 +100,7 @@ pub struct FileHash {
}
#[derive(Serialize, Deserialize, Clone)]
+#[serde(rename_all = "lowercase")]
pub enum VersionType {
Release,
Beta,
diff --git a/src/routes/mod_creation.rs b/src/routes/mod_creation.rs
index 240bbd6fa..e53de194f 100644
--- a/src/routes/mod_creation.rs
+++ b/src/routes/mod_creation.rs
@@ -1,55 +1,51 @@
-use crate::database::models::{FileHash, Mod, Team, Version, VersionFile};
-use crate::file_hosting::{upload_file, FileHostingError, UploadUrlData};
+use crate::database::models;
+use crate::file_hosting::{FileHost, FileHostingError};
use crate::models::error::ApiError;
-use crate::models::ids::random_base62;
use crate::models::mods::{GameVersion, ModId, VersionId, VersionType};
use crate::models::teams::TeamMember;
use actix_multipart::{Field, Multipart};
use actix_web::http::StatusCode;
use actix_web::web::Data;
use actix_web::{post, HttpResponse};
-use bson::doc;
-use bson::Bson;
-use chrono::Utc;
use futures::stream::StreamExt;
-use mongodb::Client;
use serde::{Deserialize, Serialize};
+use sqlx::postgres::PgPool;
use thiserror::Error;
#[derive(Error, Debug)]
pub enum CreateError {
#[error("Environment Error")]
EnvError(#[from] dotenv::Error),
- #[error("Error while adding project to database")]
- DatabaseError(#[from] mongodb::error::Error),
+ #[error("An unknown database error occured")]
+ SqlxDatabaseError(#[from] sqlx::Error),
+ #[error("Database Error: {0}")]
+ DatabaseError(#[from] models::DatabaseError),
#[error("Error while parsing multipart payload")]
MultipartError(actix_multipart::MultipartError),
- #[error("Error while parsing JSON")]
+ #[error("Error while parsing JSON: {0}")]
SerDeError(#[from] serde_json::Error),
- #[error("Error while serializing BSON")]
- BsonError(#[from] bson::ser::Error),
#[error("Error while uploading file")]
FileHostingError(#[from] FileHostingError),
#[error("{}", .0)]
MissingValueError(String),
- #[error("Error while trying to generate random ID")]
- RandomIdError,
#[error("Invalid format for mod icon: {0}")]
InvalidIconFormat(String),
+ #[error("Error with multipart data: {0}")]
+ InvalidInput(String),
}
impl actix_web::ResponseError for CreateError {
fn status_code(&self) -> StatusCode {
match self {
CreateError::EnvError(..) => StatusCode::INTERNAL_SERVER_ERROR,
+ CreateError::SqlxDatabaseError(..) => StatusCode::INTERNAL_SERVER_ERROR,
CreateError::DatabaseError(..) => StatusCode::INTERNAL_SERVER_ERROR,
CreateError::FileHostingError(..) => StatusCode::INTERNAL_SERVER_ERROR,
- CreateError::BsonError(..) => StatusCode::INTERNAL_SERVER_ERROR,
CreateError::SerDeError(..) => StatusCode::BAD_REQUEST,
CreateError::MultipartError(..) => StatusCode::BAD_REQUEST,
CreateError::MissingValueError(..) => StatusCode::BAD_REQUEST,
CreateError::InvalidIconFormat(..) => StatusCode::BAD_REQUEST,
- CreateError::RandomIdError => StatusCode::INTERNAL_SERVER_ERROR,
+ CreateError::InvalidInput(..) => StatusCode::BAD_REQUEST,
}
}
@@ -57,14 +53,14 @@ impl actix_web::ResponseError for CreateError {
HttpResponse::build(self.status_code()).json(ApiError {
error: match self {
CreateError::EnvError(..) => "environment_error",
+ CreateError::SqlxDatabaseError(..) => "database_error",
CreateError::DatabaseError(..) => "database_error",
CreateError::FileHostingError(..) => "file_hosting_error",
- CreateError::BsonError(..) => "database_error",
CreateError::SerDeError(..) => "invalid_input",
CreateError::MultipartError(..) => "invalid_input",
CreateError::MissingValueError(..) => "invalid_input",
- CreateError::RandomIdError => "id_generation_error",
CreateError::InvalidIconFormat(..) => "invalid_input",
+ CreateError::InvalidInput(..) => "invalid_input",
},
description: &self.to_string(),
})
@@ -73,13 +69,13 @@ impl actix_web::ResponseError for CreateError {
#[derive(Serialize, Deserialize, Clone)]
struct InitialVersionData {
- pub file_indexes: Vec,
+ pub file_parts: Vec,
pub version_number: String,
pub version_title: String,
pub version_body: String,
pub dependencies: Vec,
pub game_versions: Vec,
- pub version_type: VersionType,
+ pub release_channel: VersionType,
pub loaders: Vec,
}
@@ -107,295 +103,320 @@ struct ModCreateData {
pub wiki_url: Option,
}
+struct UploadedFile {
+ file_id: String,
+ file_name: String,
+}
+
+async fn undo_uploads(
+ file_host: &dyn FileHost,
+ uploaded_files: &[UploadedFile],
+) -> Result<(), CreateError> {
+ for file in uploaded_files {
+ file_host
+ .delete_file_version(&file.file_id, &file.file_name)
+ .await?;
+ }
+ Ok(())
+}
+
#[post("api/v1/mod")]
pub async fn mod_create(
- mut payload: Multipart,
- client: Data,
- upload_url: Data,
+ payload: Multipart,
+ client: Data,
+ file_host: Data>,
) -> Result {
- //TODO Switch to transactions for safer database and file upload calls (once it is implemented in the APIs)
- let cdn_url = dotenv::var("CDN_URL")?;
+ let mut transaction = client.begin().await?;
+ let mut uploaded_files = Vec::new();
- let db = client.database("modrinth");
+ let result = mod_create_inner(
+ payload,
+ &mut transaction,
+ &***file_host,
+ &mut uploaded_files,
+ )
+ .await;
- let mods = db.collection("mods");
- let versions = db.collection("versions");
+ if result.is_err() {
+ let undo_result = undo_uploads(&***file_host, &uploaded_files).await;
+ let rollback_result = transaction.rollback().await;
- let mut mod_id = ModId(random_base62(8));
- let mut retry_count = 0;
-
- //Check if ID is unique
- loop {
- let filter = doc! { "_id": mod_id.0 };
-
- if mods.find(filter, None).await?.next().await.is_some() {
- mod_id = ModId(random_base62(8));
- } else {
- break;
+ if let Err(e) = undo_result {
+ return Err(e);
}
-
- retry_count += 1;
- if retry_count > 20 {
- return Err(CreateError::RandomIdError);
+ if let Err(e) = rollback_result {
+ return Err(e.into());
}
+ } else {
+ transaction.commit().await?;
}
- let mut created_versions: Vec = vec![];
+ result
+}
+
+async fn mod_create_inner(
+ mut payload: Multipart,
+ transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
+ file_host: &dyn FileHost,
+ uploaded_files: &mut Vec,
+) -> Result {
+ let cdn_url = dotenv::var("CDN_URL")?;
+
+ let mod_id = models::generate_mod_id(transaction).await?.into();
+
+ let mut created_versions: Vec = vec![];
let mut mod_create_data: Option = None;
let mut icon_url = "".to_string();
- let mut current_file_index = 0;
while let Some(item) = payload.next().await {
let mut field: Field = item.map_err(CreateError::MultipartError)?;
let content_disposition = field.content_disposition().ok_or_else(|| {
- CreateError::MissingValueError("Missing content disposition!".to_string())
+ CreateError::MissingValueError("Missing content disposition".to_string())
})?;
let name = content_disposition
.get_name()
- .ok_or_else(|| CreateError::MissingValueError("Missing content name!".to_string()))?;
+ .ok_or_else(|| CreateError::MissingValueError("Missing content name".to_string()))?;
- while let Some(chunk) = field.next().await {
- let data = &chunk.map_err(CreateError::MultipartError)?;
-
- if name == "data" {
- mod_create_data = Some(serde_json::from_slice(&data)?);
- } else {
- let file_name = content_disposition.get_filename().ok_or_else(|| {
- CreateError::MissingValueError("Missing content file name".to_string())
- })?;
- let file_extension = if let Some(last_period) = file_name.rfind('.') {
- file_name.get(last_period + 1..).unwrap_or("")
- } else {
- return Err(CreateError::MissingValueError(
- "Missing content file extension".to_string(),
- ));
- };
-
- if let Some(create_data) = &mod_create_data {
- if name == "icon" {
- if let Some(ext) = get_image_content_type(file_extension) {
- let upload_data = upload_file(
- upload_url.get_ref(),
- ext,
- &format!("mods/icons/{}/{}", mod_id, file_name),
- data.to_vec(),
- )
- .await?;
-
- icon_url = format!("{}/{}", cdn_url, upload_data.file_name);
- } else {
- return Err(CreateError::InvalidIconFormat(file_extension.to_string()));
- }
- } else if &*file_extension == "jar" {
- let initial_version_data = create_data
- .initial_versions
- .iter()
- .position(|x| x.file_indexes.contains(¤t_file_index));
-
- if let Some(version_data_index) = initial_version_data {
- let version_data = create_data
- .initial_versions
- .get(version_data_index)
- .ok_or_else(|| {
- CreateError::MissingValueError(
- "Missing file extension!".to_string(),
- )
- })?
- .clone();
-
- let mut created_version_filter = created_versions
- .iter_mut()
- .filter(|x| x.number == version_data.version_number);
-
- match created_version_filter.next() {
- Some(created_version) => {
- let upload_data = upload_file(
- upload_url.get_ref(),
- "application/java-archive",
- &format!(
- "{}/{}/{}",
- create_data.mod_namespace.replace(".", "/"),
- version_data.version_number,
- file_name
- ),
- data.to_vec(),
- )
- .await?;
-
- created_version.files.push(VersionFile {
- game_versions: version_data
- .game_versions
- .into_iter()
- .map(|x| x.0)
- .collect(),
- hashes: vec![FileHash {
- algorithm: "sha1".to_string(),
- hash: upload_data.content_sha1,
- }],
- url: format!("{}/{}", cdn_url, upload_data.file_name),
- });
- }
- None => {
- //Check if ID is unique
- let mut version_id = VersionId(random_base62(8));
- retry_count = 0;
-
- loop {
- let filter = doc! { "_id": version_id.0 };
-
- if versions.find(filter, None).await?.next().await.is_some()
- {
- version_id = VersionId(random_base62(8));
- } else {
- break;
- }
-
- retry_count += 1;
- if retry_count > 20 {
- return Err(CreateError::RandomIdError);
- }
- }
-
- let body_url = format!(
- "data/{}/changelogs/{}/body.md",
- mod_id, version_id
- );
-
- upload_file(
- upload_url.get_ref(),
- "text/plain",
- &body_url,
- version_data.version_body.into_bytes(),
- )
- .await?;
-
- let upload_data = upload_file(
- upload_url.get_ref(),
- "application/java-archive",
- &format!(
- "{}/{}/{}",
- create_data.mod_namespace.replace(".", "/"),
- version_data.version_number,
- file_name
- ),
- data.to_vec(),
- )
- .await?;
-
- let version = Version {
- version_id: version_id.0 as i32,
- mod_id: mod_id.0 as i32,
- name: version_data.version_title,
- number: version_data.version_number.clone(),
- changelog_url: Some(format!("{}/{}", cdn_url, body_url)),
- date_published: Utc::now().to_rfc2822(),
- downloads: 0,
- version_type: version_data.version_type.to_string(),
- files: vec![VersionFile {
- game_versions: version_data
- .game_versions
- .into_iter()
- .map(|x| x.0)
- .collect::>(),
- hashes: vec![FileHash {
- algorithm: "sha1".to_string(),
- hash: upload_data.content_sha1,
- }],
- url: format!("{}/{}", cdn_url, upload_data.file_name),
- }],
- dependencies: version_data
- .dependencies
- .into_iter()
- .map(|x| x.0 as i32)
- .collect::>(),
- game_versions: vec![],
- loaders: vec![],
- };
- //TODO: Malware scan + file validation
-
- created_versions.push(version);
- }
- }
- }
- }
- }
+ if name == "data" {
+ let mut data = Vec::new();
+ while let Some(chunk) = field.next().await {
+ data.extend_from_slice(&chunk.map_err(CreateError::MultipartError)?);
}
+ mod_create_data = Some(serde_json::from_slice(&data)?);
+ continue;
}
- current_file_index += 1;
- }
-
- for version in &created_versions {
- let serialized_version = serde_json::to_string(&version)?;
- let document = Bson::from(serialized_version)
- .as_document()
- .ok_or_else(|| {
- CreateError::MissingValueError(
- "No document present for database entry!".to_string(),
- )
- })?
- .clone();
-
- versions.insert_one(document, None).await?;
- }
-
- if let Some(create_data) = mod_create_data {
- let body_url = format!("data/{}/body.md", mod_id);
-
- upload_file(
- upload_url.get_ref(),
- "text/plain",
- &body_url,
- create_data.mod_body.into_bytes(),
- )
- .await?;
-
- let created_mod: Mod = Mod {
- id: mod_id.0 as i32,
- team: Team {
- id: random_base62(8) as i32,
- members: create_data
- .team_members
- .into_iter()
- .map(|x| crate::database::models::TeamMember {
- user_id: x.user_id.0 as i32,
- name: x.name,
- role: x.role,
- })
- .collect(),
- },
- title: create_data.mod_name,
- icon_url: Some(icon_url),
- description: create_data.mod_description,
- body_url: format!("{}/{}", cdn_url, body_url),
- published: Utc::now().to_rfc2822(),
- downloads: 0,
- categories: create_data.categories,
- version_ids: created_versions
- .into_iter()
- .map(|x| x.version_id as i32)
- .collect::>(),
- issues_url: create_data.issues_url,
- source_url: create_data.source_url,
- wiki_url: create_data.wiki_url,
+ let file_name = content_disposition.get_filename().ok_or_else(|| {
+ CreateError::MissingValueError("Missing content file name".to_string())
+ })?;
+ let file_extension = if let Some(last_period) = file_name.rfind('.') {
+ file_name.get((last_period + 1)..).unwrap_or("")
+ } else {
+ return Err(CreateError::MissingValueError(
+ "Missing content file extension".to_string(),
+ ));
};
- let document = bson::to_bson(&created_mod)?
- .as_document()
- .ok_or_else(|| {
- CreateError::MissingValueError(
- "No document present for database entry!".to_string(),
- )
- })?
- .clone();
+ if name == "icon" {
+ icon_url = process_icon_upload(
+ uploaded_files,
+ mod_id,
+ file_name,
+ file_extension,
+ file_host,
+ field,
+ &cdn_url,
+ )
+ .await?;
+ continue;
+ }
- mods.insert_one(document, None).await?;
+ if &*file_extension == "jar" {
+ let create_data = mod_create_data.as_ref().ok_or_else(|| {
+ CreateError::InvalidInput(String::from("`data` field must come before file fields"))
+ })?;
+
+ let version_data = create_data
+ .initial_versions
+ .iter()
+ .find(|x| x.file_parts.iter().any(|n| n == name))
+ .ok_or_else(|| {
+ CreateError::InvalidInput(format!(
+ "Jar file `{}` (field {}) isn't specified in the versions data",
+ file_name, name
+ ))
+ })?;
+
+ // If a version has already been created for this version, add the
+ // file to it instead of creating a new version.
+
+ let created_version = if let Some(created_version) = created_versions
+ .iter_mut()
+ .find(|x| x.version_number == version_data.version_number)
+ {
+ created_version
+ } else {
+ let version_id: VersionId = models::generate_version_id(transaction).await?.into();
+
+ let body_url = format!("data/{}/changelogs/{}/body.md", mod_id, version_id);
+
+ let uploaded_text = file_host
+ .upload_file(
+ "text/plain",
+ &body_url,
+ version_data.version_body.clone().into_bytes(),
+ )
+ .await?;
+
+ uploaded_files.push(UploadedFile {
+ file_id: uploaded_text.file_id.clone(),
+ file_name: uploaded_text.file_name.clone(),
+ });
+
+ // TODO: do a real lookup for the channels
+ let release_channel = match version_data.release_channel {
+ VersionType::Release => models::ChannelId(0),
+ VersionType::Beta => models::ChannelId(2),
+ VersionType::Alpha => models::ChannelId(4),
+ };
+
+ let version = models::version_item::VersionBuilder {
+ version_id: version_id.into(),
+ mod_id: mod_id.into(),
+ name: version_data.version_title.clone(),
+ version_number: version_data.version_number.clone(),
+ changelog_url: Some(format!("{}/{}", cdn_url, body_url)),
+ files: Vec::with_capacity(1),
+ dependencies: version_data
+ .dependencies
+ .iter()
+ .map(|x| (*x).into())
+ .collect::>(),
+ // TODO: add game_versions and loaders info
+ game_versions: vec![],
+ loaders: vec![],
+ release_channel,
+ };
+
+ created_versions.push(version);
+ created_versions.last_mut().unwrap()
+ };
+
+ // Upload the new jar file
+
+ let mut data = Vec::new();
+ while let Some(chunk) = field.next().await {
+ data.extend_from_slice(&chunk.map_err(CreateError::MultipartError)?);
+ }
+
+ let upload_data = file_host
+ .upload_file(
+ "application/java-archive",
+ &format!(
+ "{}/{}/{}",
+ create_data.mod_namespace.replace(".", "/"),
+ version_data.version_number,
+ file_name
+ ),
+ data.to_vec(),
+ )
+ .await?;
+
+ uploaded_files.push(UploadedFile {
+ file_id: upload_data.file_id.clone(),
+ file_name: upload_data.file_name.clone(),
+ });
+
+ // Add the newly uploaded file to the existing or new version
+
+ // TODO: Malware scan + file validation
+ created_version
+ .files
+ .push(models::version_item::VersionFileBuilder {
+ filename: file_name.to_string(),
+ url: format!("{}/{}", cdn_url, upload_data.file_name),
+ hashes: vec![models::version_item::HashBuilder {
+ algorithm: "sha1".to_string(),
+ // This is an invalid cast - the database expects the hash's
+ // bytes, but this is the string version.
+ hash: upload_data.content_sha1.into_bytes(),
+ }],
+ });
+ }
}
+ let create_data = if let Some(create_data) = mod_create_data {
+ create_data
+ } else {
+ return Err(CreateError::InvalidInput(String::from(
+ "Multipart upload missing `data` field",
+ )));
+ };
+
+ let body_url = format!("data/{}/body.md", mod_id);
+
+ let upload_data = file_host
+ .upload_file("text/plain", &body_url, create_data.mod_body.into_bytes())
+ .await?;
+
+ uploaded_files.push(UploadedFile {
+ file_id: upload_data.file_id.clone(),
+ file_name: upload_data.file_name.clone(),
+ });
+
+ let team = models::team_item::TeamBuilder {
+ members: create_data
+ .team_members
+ .into_iter()
+ .map(|member| models::team_item::TeamMemberBuilder {
+ user_id: member.user_id.into(),
+ name: member.name,
+ role: member.role,
+ })
+ .collect(),
+ };
+
+ let team_id = team.insert(&mut *transaction).await?;
+
+ // Insert the new mod into the database
+
+ let mod_builder = models::mod_item::ModBuilder {
+ mod_id: mod_id.into(),
+ team_id,
+ title: create_data.mod_name,
+ description: create_data.mod_description,
+ body_url: format!("{}/{}", cdn_url, body_url),
+ icon_url: Some(icon_url),
+ issues_url: create_data.issues_url,
+ source_url: create_data.source_url,
+ wiki_url: create_data.wiki_url,
+
+ // TODO: convert `create_data.categories` from Vec to Vec
+ categories: Vec::new(),
+ initial_versions: created_versions,
+ };
+
+ let _mod_id = mod_builder.insert(&mut *transaction).await?;
+
+ // TODO: respond with the new mod info, or with just the new mod id.
Ok(HttpResponse::Ok().into())
}
+async fn process_icon_upload(
+ uploaded_files: &mut Vec,
+ mod_id: ModId,
+ file_name: &str,
+ file_extension: &str,
+ file_host: &dyn FileHost,
+ mut field: actix_multipart::Field,
+ cdn_url: &str,
+) -> Result {
+ if let Some(content_type) = get_image_content_type(file_extension) {
+ let mut data = Vec::new();
+ while let Some(chunk) = field.next().await {
+ data.extend_from_slice(&chunk.map_err(CreateError::MultipartError)?);
+ }
+
+ let upload_data = file_host
+ .upload_file(
+ content_type,
+ &format!("mods/icons/{}/{}", mod_id, file_name),
+ data,
+ )
+ .await?;
+
+ uploaded_files.push(UploadedFile {
+ file_id: upload_data.file_id.clone(),
+ file_name: upload_data.file_name.clone(),
+ });
+
+ Ok(format!("{}/{}", cdn_url, upload_data.file_name))
+ } else {
+ Err(CreateError::InvalidIconFormat(file_extension.to_string()))
+ }
+}
+
fn get_image_content_type(extension: &str) -> Option<&'static str> {
let content_type = match &*extension {
"bmp" => "image/bmp",
diff --git a/src/search/indexing/curseforge_import.rs b/src/search/indexing/curseforge_import.rs
index 39ba5256c..afc666809 100644
--- a/src/search/indexing/curseforge_import.rs
+++ b/src/search/indexing/curseforge_import.rs
@@ -178,7 +178,7 @@ pub async fn index_curseforge(
.replace("/256/256/", "/64/64/");
docs_to_add.push(SearchMod {
- mod_id: -curseforge_mod.id,
+ mod_id: -curseforge_mod.id as i64,
author: (&curseforge_mod.authors[0].name).to_string(),
title: curseforge_mod.name,
description: curseforge_mod.summary.chars().take(150).collect(),
diff --git a/src/search/indexing/local_import.rs b/src/search/indexing/local_import.rs
index 01c85487d..0229ac5d0 100644
--- a/src/search/indexing/local_import.rs
+++ b/src/search/indexing/local_import.rs
@@ -1,70 +1,77 @@
-use bson::doc;
-use futures::StreamExt;
+use futures::{StreamExt, TryStreamExt};
use log::info;
-use crate::database::models::Item;
-use crate::database::{DatabaseError, Mod, Version};
-
use super::IndexingError;
use crate::search::SearchMod;
+use sqlx::postgres::PgPool;
-pub async fn index_local(client: mongodb::Client) -> Result, IndexingError> {
+pub async fn index_local(pool: PgPool) -> Result, IndexingError> {
info!("Indexing local mods!");
let mut docs_to_add: Vec = vec![];
- let db = client.database("modrinth");
+ let mut results = sqlx::query!(
+ "
+ SELECT m.id, m.title, m.description, m.downloads, m.icon_url, m.body_url, m.published FROM mods m
+ "
+ )
+ .fetch(&pool);
- let mods = db.collection("mods");
- let versions = db.collection("versions");
+ while let Some(result) = results.next().await {
+ if let Ok(result) = result {
+ let versions: Vec = sqlx::query!(
+ "
+ SELECT gv.version FROM versions
+ INNER JOIN game_versions_versions gvv ON gvv.joining_version_id=versions.id
+ INNER JOIN game_versions gv ON gvv.game_version_id=gv.id
+ WHERE versions.mod_id = $1
+ ",
+ result.id
+ )
+ .fetch_many(&pool)
+ .try_filter_map(|e| async { Ok(e.right().map(|c| c.version)) })
+ .try_collect::>()
+ .await?;
- let mut results = mods
- .find(None, None)
- .await
- .map_err(DatabaseError::LocalDatabaseError)?;
+ let categories = sqlx::query!(
+ "
+ SELECT c.category
+ FROM mods_categories mc
+ INNER JOIN categories c ON mc.joining_category_id=c.id
+ WHERE mc.joining_mod_id = $1
+ ",
+ result.id
+ )
+ .fetch_many(&pool)
+ .try_filter_map(|e| async { Ok(e.right().map(|c| c.category)) })
+ .try_collect::>()
+ .await?;
- while let Some(unparsed_result) = results.next().await {
- let result: Mod =
- *Mod::from_doc(unparsed_result.map_err(DatabaseError::LocalDatabaseError)?)?;
+ let mut icon_url = "".to_string();
- let mut mod_versions = versions
- .find(doc! { "mod_id": result.id }, None)
- .await
- .map_err(DatabaseError::LocalDatabaseError)?;
+ if let Some(url) = result.icon_url {
+ icon_url = url;
+ }
- let mut mod_game_versions = vec![];
-
- while let Some(unparsed_version) = mod_versions.next().await {
- let mut version = unparsed_version
- .map_err(DatabaseError::LocalDatabaseError)
- .and_then(Version::from_doc)?;
- mod_game_versions.append(&mut version.game_versions);
+ docs_to_add.push(SearchMod {
+ mod_id: result.id,
+ author: "".to_string(),
+ title: result.title,
+ description: result.description,
+ keywords: categories,
+ versions,
+ downloads: result.downloads,
+ page_url: result.body_url,
+ icon_url,
+ author_url: "".to_string(),
+ date_created: result.published.to_string(),
+ created: 0,
+ date_modified: "".to_string(),
+ updated: 0,
+ latest_version: "".to_string(),
+ empty: String::from("{}{}{}"),
+ });
}
-
- let mut icon_url = "".to_string();
-
- if let Some(url) = result.icon_url {
- icon_url = url;
- }
-
- docs_to_add.push(SearchMod {
- mod_id: result.id,
- author: "".to_string(),
- title: result.title,
- description: result.description,
- keywords: result.categories,
- versions: mod_game_versions,
- downloads: result.downloads,
- page_url: "".to_string(),
- icon_url,
- author_url: "".to_string(),
- date_created: "".to_string(),
- created: 0,
- date_modified: "".to_string(),
- updated: 0,
- latest_version: "".to_string(),
- empty: String::from("{}{}{}"),
- });
}
Ok(docs_to_add)
diff --git a/src/search/indexing/mod.rs b/src/search/indexing/mod.rs
index f06d6392f..3317679b9 100644
--- a/src/search/indexing/mod.rs
+++ b/src/search/indexing/mod.rs
@@ -7,6 +7,7 @@ use crate::search::indexing::local_import::index_local;
use crate::search::SearchMod;
use meilisearch_sdk::client::Client;
use meilisearch_sdk::settings::Settings;
+use sqlx::postgres::PgPool;
use std::collections::{HashMap, VecDeque};
use thiserror::Error;
@@ -21,7 +22,7 @@ pub enum IndexingError {
#[error("Error while parsing a timestamp: {0}")]
ParseDateError(#[from] chrono::format::ParseError),
#[error("Database Error: {0}")]
- DatabaseError(#[from] crate::database::DatabaseError),
+ DatabaseError(#[from] sqlx::error::Error),
#[error("Environment Error")]
EnvError(#[from] dotenv::Error),
}
@@ -31,14 +32,14 @@ pub enum IndexingError {
// assumes a max average size of 1KiB per mod to avoid this cap.
const MEILISEARCH_CHUNK_SIZE: usize = 10000;
-pub async fn index_mods(db: mongodb::Client) -> Result<(), IndexingError> {
+pub async fn index_mods(pool: PgPool) -> Result<(), IndexingError> {
// Check if the index exists
let address = &*dotenv::var("MEILISEARCH_ADDR")?;
let client = Client::new(address, "");
let mut docs_to_add: Vec = vec![];
- docs_to_add.append(&mut index_local(db.clone()).await?);
+ docs_to_add.append(&mut index_local(pool.clone()).await?);
if dotenv::var("INDEX_CURSEFORGE")?
.parse()
.expect("`INDEX_CURSEFORGE` is not a boolean.")
diff --git a/src/search/mod.rs b/src/search/mod.rs
index 4edeb2b72..c5b37d393 100644
--- a/src/search/mod.rs
+++ b/src/search/mod.rs
@@ -47,7 +47,7 @@ impl actix_web::ResponseError for SearchError {
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct SearchMod {
- pub mod_id: i32,
+ pub mod_id: i64,
pub author: String,
pub title: String,
pub description: String,
@@ -66,7 +66,7 @@ pub struct SearchMod {
}
impl Document for SearchMod {
- type UIDType = i32;
+ type UIDType = i64;
fn get_uid(&self) -> &Self::UIDType {
&self.mod_id