Compare commits
3 Commits
aa47ec7c43
...
7deb8e5bfc
Author | SHA1 | Date | |
---|---|---|---|
7deb8e5bfc | |||
b77b1bc139 | |||
a0c5122735 |
155
Cargo.lock
generated
155
Cargo.lock
generated
@ -150,12 +150,6 @@ version = "0.22.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "1.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "2.5.0"
|
||||
@ -187,6 +181,18 @@ version = "3.16.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c"
|
||||
|
||||
[[package]]
|
||||
name = "bytemuck"
|
||||
version = "1.16.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b236fc92302c97ed75b38da1f4917b5cdda4984745740f153a5d3059e48d725e"
|
||||
|
||||
[[package]]
|
||||
name = "byteorder"
|
||||
version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
|
||||
|
||||
[[package]]
|
||||
name = "bytes"
|
||||
version = "1.6.0"
|
||||
@ -239,9 +245,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "chrono-tz"
|
||||
version = "0.8.6"
|
||||
version = "0.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d59ae0466b83e838b81a54256c39d5d7c20b9d7daa10510a242d9b75abd5936e"
|
||||
checksum = "93698b29de5e97ad0ae26447b344c482a7284c737d9ddc5f9e52b74a336671bb"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"chrono-tz-build",
|
||||
@ -250,9 +256,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "chrono-tz-build"
|
||||
version = "0.2.1"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "433e39f13c9a060046954e0592a8d0a4bcb1040125cbf91cb8ee58964cfb350f"
|
||||
checksum = "0c088aee841df9c3041febbb73934cfc39708749bf96dc827e3359cd39ef11b1"
|
||||
dependencies = [
|
||||
"parse-zoneinfo",
|
||||
"phf",
|
||||
@ -261,9 +267,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "clap"
|
||||
version = "4.5.4"
|
||||
version = "4.5.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "90bc066a67923782aa8515dbaea16946c5bcc5addbd668bb80af688e53e548a0"
|
||||
checksum = "5db83dced34638ad474f39f250d7fea9598bdd239eaced1bdf45d597da0f433f"
|
||||
dependencies = [
|
||||
"clap_builder",
|
||||
"clap_derive",
|
||||
@ -271,9 +277,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "clap_builder"
|
||||
version = "4.5.2"
|
||||
version = "4.5.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ae129e2e766ae0ec03484e609954119f123cc1fe650337e155d03b022f24f7b4"
|
||||
checksum = "f7e204572485eb3fbf28f871612191521df159bc3e15a9f5064c66dba3a8c05f"
|
||||
dependencies = [
|
||||
"anstream",
|
||||
"anstyle",
|
||||
@ -283,9 +289,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "clap_derive"
|
||||
version = "4.5.4"
|
||||
version = "4.5.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "528131438037fd55894f62d6e9f068b8f45ac57ffa77517819645d10aed04f64"
|
||||
checksum = "c780290ccf4fb26629baa7a1081e68ced113f1d3ec302fa5948f1c381ebf06c6"
|
||||
dependencies = [
|
||||
"heck",
|
||||
"proc-macro2",
|
||||
@ -447,6 +453,12 @@ dependencies = [
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "either"
|
||||
version = "1.13.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0"
|
||||
|
||||
[[package]]
|
||||
name = "equivalent"
|
||||
version = "1.0.1"
|
||||
@ -601,11 +613,11 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "globwalk"
|
||||
version = "0.8.1"
|
||||
version = "0.9.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "93e3af942408868f6934a7b85134a3230832b9977cf66125df2f9edcfce4ddcc"
|
||||
checksum = "0bf760ebf69878d9fd8f110c89703d90ce35095324d1f1edcb595c63945ee757"
|
||||
dependencies = [
|
||||
"bitflags 1.3.2",
|
||||
"bitflags",
|
||||
"ignore",
|
||||
"walkdir",
|
||||
]
|
||||
@ -832,6 +844,20 @@ dependencies = [
|
||||
"winapi-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "image"
|
||||
version = "0.25.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fd54d660e773627692c524beaad361aca785a4f9f5730ce91f42aabe5bce3d11"
|
||||
dependencies = [
|
||||
"bytemuck",
|
||||
"byteorder",
|
||||
"num-traits",
|
||||
"rayon",
|
||||
"zune-core",
|
||||
"zune-jpeg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "indexmap"
|
||||
version = "1.9.3"
|
||||
@ -912,11 +938,12 @@ checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89"
|
||||
|
||||
[[package]]
|
||||
name = "little-hesinde"
|
||||
version = "0.2.3"
|
||||
version = "0.3.0"
|
||||
dependencies = [
|
||||
"calibre-db",
|
||||
"clap",
|
||||
"ignore",
|
||||
"image",
|
||||
"once_cell",
|
||||
"poem",
|
||||
"quick-xml",
|
||||
@ -924,6 +951,7 @@ dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_with",
|
||||
"sha2",
|
||||
"tera",
|
||||
"thiserror",
|
||||
"time",
|
||||
@ -1005,7 +1033,7 @@ version = "0.28.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ab2156c4fce2f8df6c499cc1c763e4394b7482525bf2a9701c9d79d215f519e4"
|
||||
dependencies = [
|
||||
"bitflags 2.5.0",
|
||||
"bitflags",
|
||||
"cfg-if",
|
||||
"cfg_aliases",
|
||||
"libc",
|
||||
@ -1208,9 +1236,9 @@ checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec"
|
||||
|
||||
[[package]]
|
||||
name = "poem"
|
||||
version = "3.0.0"
|
||||
version = "3.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8b735eaaaa6bc7ed2dcbcab1d5373afe1f6d03a37d8695ba3c42101f733a8455"
|
||||
checksum = "e88b6912ed1e8833d7c22c9c986c517f4518d7d37e3c04566d917c789aaea591"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"futures-util",
|
||||
@ -1356,13 +1384,33 @@ dependencies = [
|
||||
"getrandom",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rayon"
|
||||
version = "1.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa"
|
||||
dependencies = [
|
||||
"either",
|
||||
"rayon-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rayon-core"
|
||||
version = "1.12.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2"
|
||||
dependencies = [
|
||||
"crossbeam-deque",
|
||||
"crossbeam-utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "redox_syscall"
|
||||
version = "0.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "469052894dcb553421e483e4209ee581a45100d31b4018de03e5a7ad86374a7e"
|
||||
dependencies = [
|
||||
"bitflags 2.5.0",
|
||||
"bitflags",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -1409,7 +1457,7 @@ version = "0.31.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b838eba278d213a8beaf485bd313fd580ca4505a00d5871caeb1457c55322cae"
|
||||
dependencies = [
|
||||
"bitflags 2.5.0",
|
||||
"bitflags",
|
||||
"fallible-iterator",
|
||||
"fallible-streaming-iterator",
|
||||
"hashlink",
|
||||
@ -1420,9 +1468,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rust-embed"
|
||||
version = "8.3.0"
|
||||
version = "8.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fb78f46d0066053d16d4ca7b898e9343bc3530f71c61d5ad84cd404ada068745"
|
||||
checksum = "19549741604902eb99a7ed0ee177a0663ee1eda51a29f71401f166e47e77806a"
|
||||
dependencies = [
|
||||
"rust-embed-impl",
|
||||
"rust-embed-utils",
|
||||
@ -1431,9 +1479,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rust-embed-impl"
|
||||
version = "8.3.0"
|
||||
version = "8.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b91ac2a3c6c0520a3fb3dd89321177c3c692937c4eb21893378219da10c44fc8"
|
||||
checksum = "cb9f96e283ec64401f30d3df8ee2aaeb2561f34c824381efa24a35f79bf40ee4"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@ -1444,9 +1492,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rust-embed-utils"
|
||||
version = "8.3.0"
|
||||
version = "8.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "86f69089032567ffff4eada41c573fc43ff466c7db7c5688b2e7969584345581"
|
||||
checksum = "38c74a686185620830701348de757fd36bef4aa9680fd23c49fc539ddcc1af32"
|
||||
dependencies = [
|
||||
"sha2",
|
||||
"walkdir",
|
||||
@ -1464,7 +1512,7 @@ version = "0.38.34"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "70dc5ec042f7a43c4a73241207cecc9873a06d45debb38b329f8541d85c2730f"
|
||||
dependencies = [
|
||||
"bitflags 2.5.0",
|
||||
"bitflags",
|
||||
"errno",
|
||||
"libc",
|
||||
"linux-raw-sys",
|
||||
@ -1503,18 +1551,18 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.200"
|
||||
version = "1.0.203"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ddc6f9cc94d67c0e21aaf7eda3a010fd3af78ebf6e096aa6e2e13c79749cce4f"
|
||||
checksum = "7253ab4de971e72fb7be983802300c30b5a7f0c2e56fab8abfc6a214307c0094"
|
||||
dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.200"
|
||||
version = "1.0.203"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "856f046b9400cee3c8c94ed572ecdb752444c24528c035cd35882aad6f492bcb"
|
||||
checksum = "500cbc0ebeb6f46627f50f3f5811ccf6bf00643be300b4c3eabc0ef55dc5b5ba"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@ -1523,9 +1571,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "serde_json"
|
||||
version = "1.0.116"
|
||||
version = "1.0.118"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3e17db7126d17feb94eb3fad46bf1a96b034e8aacbc2e775fe81505f8b0b2813"
|
||||
checksum = "d947f6b3163d8857ea16c4fa0dd4840d52f3041039a85decd46867eb1abef2e4"
|
||||
dependencies = [
|
||||
"itoa",
|
||||
"ryu",
|
||||
@ -1707,9 +1755,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tera"
|
||||
version = "1.19.1"
|
||||
version = "1.20.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "970dff17c11e884a4a09bc76e3a17ef71e01bb13447a11e85226e254fe6d10b8"
|
||||
checksum = "ab9d851b45e865f178319da0abdbfe6acbc4328759ff18dafc3a41c16b4cd2ee"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"chrono-tz",
|
||||
@ -1790,9 +1838,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tokio"
|
||||
version = "1.37.0"
|
||||
version = "1.38.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1adbebffeca75fcfd058afa480fb6c0b81e165a0323f9c9d39c9697e37c46787"
|
||||
checksum = "ba4f4a02a7a80d6f274636f0aa95c7e383b912d41fe721a31f29e29698585a4a"
|
||||
dependencies = [
|
||||
"backtrace",
|
||||
"bytes",
|
||||
@ -1808,9 +1856,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tokio-macros"
|
||||
version = "2.2.0"
|
||||
version = "2.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b"
|
||||
checksum = "5f5ae998a069d4b5aba8ee9dad856af7d520c3699e6159b185c2acd48155d39a"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@ -1998,9 +2046,9 @@ checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a"
|
||||
|
||||
[[package]]
|
||||
name = "uuid"
|
||||
version = "1.8.0"
|
||||
version = "1.9.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a183cf7feeba97b4dd1c0d46788634f6221d87fa961b305bed08c851829efcc0"
|
||||
checksum = "5de17fd2f7da591098415cff336e12965a28061ddace43b59cb3c430179c9439"
|
||||
dependencies = [
|
||||
"getrandom",
|
||||
"rand",
|
||||
@ -2338,3 +2386,18 @@ dependencies = [
|
||||
"once_cell",
|
||||
"simd-adler32",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zune-core"
|
||||
version = "0.4.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3f423a2c17029964870cfaabb1f13dfab7d092a62a29a89264f4d36990ca414a"
|
||||
|
||||
[[package]]
|
||||
name = "zune-jpeg"
|
||||
version = "0.4.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ec866b44a2a1fd6133d363f073ca1b179f438f99e7e5bfb1e33f7181facfe448"
|
||||
dependencies = [
|
||||
"zune-core",
|
||||
]
|
||||
|
@ -5,8 +5,8 @@ members = [
|
||||
]
|
||||
|
||||
[workspace.dependencies]
|
||||
serde = "1.0.200"
|
||||
thiserror = "1.0.59"
|
||||
serde = "1.0.203"
|
||||
thiserror = "1.0.61"
|
||||
time = { version = "0.3.36", features = ["macros", "serde", "formatting", "parsing" ] }
|
||||
|
||||
[workspace.package]
|
||||
|
@ -1,5 +1,8 @@
|
||||
FROM docker.io/rust:1-alpine3.20 AS builder
|
||||
|
||||
RUN mkdir /tmp/tmp
|
||||
RUN echo "hesinde:x:2222:2222:Linux User,,,:/:/app" > /passwd
|
||||
|
||||
RUN apk --no-cache add \
|
||||
musl-dev
|
||||
|
||||
@ -13,12 +16,16 @@ COPY . .
|
||||
RUN cargo build --release --target=$(arch)-unknown-linux-musl
|
||||
RUN cp "./target/$(arch)-unknown-linux-musl/release/little-hesinde" /app
|
||||
|
||||
|
||||
FROM scratch
|
||||
|
||||
COPY --from=builder /passwd /etc/passwd
|
||||
COPY --from=builder /app /app
|
||||
CMD ["/app", "--listen-address", "[::]:3000", "--", "/library"]
|
||||
COPY --from=builder --chown=2222: /tmp/tmp /tmp
|
||||
|
||||
ENV TMPDIR=/
|
||||
VOLUME ["/library"]
|
||||
USER hesinde
|
||||
|
||||
CMD ["/app", "--listen-address", "[::]:3000", "--cache-path", "/tmp/cache", "--", "/library"]
|
||||
|
||||
ENV TMPDIR=/tmp
|
||||
VOLUME ["/library", "/tmp"]
|
||||
EXPOSE 3000
|
||||
|
13
README.md
13
README.md
@ -37,12 +37,17 @@ From there on `cargo run` and `cargo build` and so on can be used.
|
||||
Usage: little-hesinde [OPTIONS] -- <LIBRARY_PATH>
|
||||
|
||||
Arguments:
|
||||
<LIBRARY_PATH> Calibre library path
|
||||
<LIBRARY_PATH> Calibre library path [env: LIBRARY_PATH=]
|
||||
|
||||
Options:
|
||||
-l, --listen-address <LISTEN_ADDRESS> Address to listen on [default: [::1]:3000]
|
||||
-h, --help Print help
|
||||
-V, --version Print version
|
||||
-l, --listen-address <LISTEN_ADDRESS>
|
||||
Address to listen on [env: LISTEN_ADDRESS=] [default: [::1]:3000]
|
||||
-c, --cache-path <CACHE_PATH>
|
||||
Cache path ($TMP cascades through $XDG_CACHE_HOME, $TMPDIR and /tmp) [env: CACHE_PATH=] [default: $TMP/little-hesinde]
|
||||
-h, --help
|
||||
Print help
|
||||
-V, --version
|
||||
Print version
|
||||
```
|
||||
|
||||
Example: `little-hesinde -l [::]4000 -- ~/Documents/library/`
|
||||
|
@ -1,15 +1,16 @@
|
||||
//! Bundle all functions together.
|
||||
|
||||
use std::path::Path;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use r2d2::Pool;
|
||||
use r2d2_sqlite::SqliteConnectionManager;
|
||||
use tempfile::NamedTempFile;
|
||||
|
||||
use crate::{
|
||||
data::{
|
||||
author::Author, book::Book, error::DataStoreError, pagination::SortOrder, series::Series,
|
||||
},
|
||||
search::{self, search},
|
||||
search::search,
|
||||
};
|
||||
|
||||
/// Top level calibre functions, bundling all sub functions in one place and providing secure access to
|
||||
@ -17,6 +18,7 @@ use crate::{
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Calibre {
|
||||
pool: Pool<SqliteConnectionManager>,
|
||||
search_db_path: PathBuf,
|
||||
}
|
||||
|
||||
impl Calibre {
|
||||
@ -27,16 +29,20 @@ impl Calibre {
|
||||
let manager = SqliteConnectionManager::file(path);
|
||||
let pool = r2d2::Pool::new(manager)?;
|
||||
|
||||
search::attach(&pool)?;
|
||||
let tmpfile = NamedTempFile::new()?;
|
||||
let (_, search_db_path) = tmpfile.keep()?;
|
||||
|
||||
Ok(Self { pool })
|
||||
Ok(Self {
|
||||
pool,
|
||||
search_db_path,
|
||||
})
|
||||
}
|
||||
|
||||
/// Full text search with a query.
|
||||
///
|
||||
/// See https://www.sqlite.org/fts5.html#full_text_query_syntax for syntax.
|
||||
pub fn search(&self, query: &str) -> Result<Vec<Book>, DataStoreError> {
|
||||
search(query, &self.pool)
|
||||
search(query, &self.pool, &self.search_db_path)
|
||||
}
|
||||
|
||||
/// Fetch book data from calibre, starting at `cursor`, fetching up to an amount of `limit` and
|
||||
|
@ -5,10 +5,11 @@
|
||||
//! virtual table leveraging fts5 (https://www.sqlite.org/fts5.html). Full-text search is run on
|
||||
//! that virtual table.
|
||||
|
||||
use std::path::Path;
|
||||
|
||||
use r2d2::{Pool, PooledConnection};
|
||||
use r2d2_sqlite::SqliteConnectionManager;
|
||||
use rusqlite::named_params;
|
||||
use tempfile::NamedTempFile;
|
||||
|
||||
use crate::data::{book::Book, error::DataStoreError};
|
||||
|
||||
@ -32,28 +33,53 @@ const SEARCH_INIT_QUERY: &str = "INSERT INTO search.fts(book_id, data)
|
||||
LEFT JOIN main.series AS s ON b2s.series = s.id
|
||||
GROUP BY b.id";
|
||||
|
||||
/// Attach the fts temporary database to the read-only calibre database.
|
||||
pub(crate) fn attach(pool: &Pool<SqliteConnectionManager>) -> Result<(), DataStoreError> {
|
||||
let conn = pool.get()?;
|
||||
let tmpfile = NamedTempFile::new()?;
|
||||
let (_, path) = tmpfile.keep()?;
|
||||
/// Ensure the search database is attached to the connection and
|
||||
/// initializes the data if needed.
|
||||
fn ensure_search_db(
|
||||
conn: &PooledConnection<SqliteConnectionManager>,
|
||||
db_path: &Path,
|
||||
) -> Result<(), DataStoreError> {
|
||||
let mut stmt =
|
||||
conn.prepare("SELECT COUNT() FROM pragma_database_list WHERE name = 'search'")?;
|
||||
let count: u64 = stmt.query_row([], |x| x.get(0))?;
|
||||
let need_attachment = count == 0;
|
||||
|
||||
if need_attachment {
|
||||
attach(conn, db_path)?;
|
||||
init(conn)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Attach the fts temporary database to the read-only calibre database.
|
||||
fn attach(
|
||||
conn: &PooledConnection<SqliteConnectionManager>,
|
||||
db_path: &Path,
|
||||
) -> Result<(), DataStoreError> {
|
||||
conn.execute(
|
||||
&format!("ATTACH DATABASE '{}' AS search", path.to_string_lossy()),
|
||||
&format!("ATTACH DATABASE '{}' AS search", db_path.to_string_lossy()),
|
||||
[],
|
||||
)?;
|
||||
init(&conn)?;
|
||||
init(conn)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Initialise the fts virtual table.
|
||||
fn init(conn: &PooledConnection<SqliteConnectionManager>) -> Result<(), DataStoreError> {
|
||||
conn.execute(
|
||||
"CREATE VIRTUAL TABLE search.fts USING fts5(book_id, data)",
|
||||
[],
|
||||
)?;
|
||||
conn.execute(SEARCH_INIT_QUERY, [])?;
|
||||
let mut stmt = conn
|
||||
.prepare("SELECT COUNT() FROM search.sqlite_master WHERE type='table' AND name = 'fts'")?;
|
||||
let count: u64 = stmt.query_row([], |x| x.get(0))?;
|
||||
let need_init = count == 0;
|
||||
|
||||
if need_init {
|
||||
conn.execute(
|
||||
"CREATE VIRTUAL TABLE search.fts USING fts5(book_id, data)",
|
||||
[],
|
||||
)?;
|
||||
conn.execute(SEARCH_INIT_QUERY, [])?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@ -62,8 +88,10 @@ fn init(conn: &PooledConnection<SqliteConnectionManager>) -> Result<(), DataStor
|
||||
pub(crate) fn search(
|
||||
query: &str,
|
||||
pool: &Pool<SqliteConnectionManager>,
|
||||
search_db_path: &Path,
|
||||
) -> Result<Vec<Book>, DataStoreError> {
|
||||
let conn = pool.get()?;
|
||||
ensure_search_db(&conn, search_db_path)?;
|
||||
|
||||
let mut stmt =
|
||||
conn.prepare("SELECT book_id FROM search.fts WHERE data MATCH (:query) ORDER BY rank")?;
|
||||
|
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "little-hesinde"
|
||||
version = "0.2.3"
|
||||
version = "0.3.0"
|
||||
edition = "2021"
|
||||
license = { workspace = true }
|
||||
authors = { workspace = true }
|
||||
@ -9,21 +9,23 @@ description = "A very simple ebook server for a calibre library, providing a htm
|
||||
|
||||
[dependencies]
|
||||
calibre-db = { path = "../calibre-db/", version = "0.1.0" }
|
||||
clap = { version = "4.5.4", features = ["derive"] }
|
||||
clap = { version = "4.5.7", features = ["derive", "env"] }
|
||||
image = { version = "0.25.1", default-features = false, features = ["jpeg", "rayon"] }
|
||||
once_cell = "1.19.0"
|
||||
poem = { version = "3.0.0", features = ["embed", "static-files"] }
|
||||
rust-embed = "8.3.0"
|
||||
poem = { version = "3.0.1", features = ["embed", "static-files"] }
|
||||
rust-embed = "8.4.0"
|
||||
sha2 = "0.10.8"
|
||||
serde = { workspace = true }
|
||||
serde_json = "1.0.116"
|
||||
serde_json = "1.0.118"
|
||||
serde_with = "3.8.1"
|
||||
tera = "1.19.1"
|
||||
tera = "1.20.0"
|
||||
thiserror = { workspace = true }
|
||||
time = { workspace = true }
|
||||
tokio = { version = "1.37.0", features = ["signal", "rt-multi-thread", "macros"] }
|
||||
tokio = { version = "1.38.0", features = ["signal", "rt-multi-thread", "macros"] }
|
||||
tokio-util = "0.7.11"
|
||||
tracing = "0.1.40"
|
||||
tracing-subscriber = "0.3.18"
|
||||
uuid = { version = "1.8.0", features = ["v4", "fast-rng"] }
|
||||
uuid = { version = "1.9.1", features = ["v4", "fast-rng"] }
|
||||
quick-xml = { version = "0.34.0", features = ["serialize"] }
|
||||
|
||||
[build-dependencies]
|
||||
|
86
little-hesinde/src/cache.rs
Normal file
86
little-hesinde/src/cache.rs
Normal file
@ -0,0 +1,86 @@
|
||||
//! Handle caching of files, specifically book covers.
|
||||
|
||||
use std::{
|
||||
fs::{self, File},
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
use sha2::{
|
||||
digest::{generic_array::GenericArray, typenum::U32},
|
||||
Digest, Sha256,
|
||||
};
|
||||
use std::fmt::Write;
|
||||
use thiserror::Error;
|
||||
use tracing::debug;
|
||||
|
||||
/// Errors from dealing with file caching.
|
||||
#[derive(Error, Debug)]
|
||||
pub enum CacheError {
|
||||
/// Error converting a hash to its string representation.
|
||||
#[error("failed to access thumbnail")]
|
||||
HashError(#[from] std::fmt::Error),
|
||||
/// Error creating a thumbnail for an image..
|
||||
#[error("failed to create thumbnail")]
|
||||
ImageError(#[from] image::ImageError),
|
||||
/// Error accessing a thumbnail.
|
||||
#[error("failed to access thumbnail")]
|
||||
ThumbnailAccessError(#[from] std::io::Error),
|
||||
/// Error accessing thumbnail directories.
|
||||
#[error("failed to access thumbnail directory")]
|
||||
ThumbnailPathError(PathBuf),
|
||||
}
|
||||
|
||||
/// Convert a hash into its path representation inside the cache directory.
|
||||
///
|
||||
/// First hash character is the top folder, second character the second level folder and the rest
|
||||
/// is the filename.
|
||||
fn hash_to_path(hash: GenericArray<u8, U32>, cache_path: &Path) -> Result<PathBuf, CacheError> {
|
||||
let mut hash_string = String::new();
|
||||
for byte in hash {
|
||||
write!(&mut hash_string, "{:02x}", byte)?;
|
||||
}
|
||||
let hash = hash_string;
|
||||
|
||||
let first_segment = &hash[0..1];
|
||||
let second_segment = &hash[1..2];
|
||||
let remaining_segment = &hash[2..];
|
||||
|
||||
Ok(PathBuf::from(cache_path)
|
||||
.join(first_segment)
|
||||
.join(second_segment)
|
||||
.join(remaining_segment))
|
||||
}
|
||||
|
||||
fn create_thumbnail(cover_path: &Path, thumbnail_path: &Path) -> Result<(), CacheError> {
|
||||
debug!("creating thumbnail for {}", cover_path.to_string_lossy());
|
||||
let folders = thumbnail_path
|
||||
.parent()
|
||||
.ok_or_else(|| CacheError::ThumbnailPathError(thumbnail_path.to_path_buf()))?;
|
||||
fs::create_dir_all(folders)?;
|
||||
|
||||
const THUMBNAIL_SIZE: u32 = 512;
|
||||
let img = image::open(cover_path)?;
|
||||
let thumbnail = img.thumbnail(THUMBNAIL_SIZE, THUMBNAIL_SIZE);
|
||||
thumbnail.save_with_format(thumbnail_path, image::ImageFormat::Jpeg)?;
|
||||
debug!("saved thumbnail to {}", thumbnail_path.to_string_lossy());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get the thumbnail for a book cover.
|
||||
///
|
||||
/// If a thumbnail does not yet exist, create it.
|
||||
pub fn get_thumbnail(cover_path: &Path, cache_path: &Path) -> Result<File, CacheError> {
|
||||
let path_str = cover_path.to_string_lossy();
|
||||
let mut hasher = Sha256::new();
|
||||
hasher.update(path_str.as_bytes());
|
||||
let hash = hasher.finalize();
|
||||
|
||||
let thumbnail_path = hash_to_path(hash, cache_path)?;
|
||||
|
||||
if !thumbnail_path.exists() {
|
||||
create_thumbnail(cover_path, &thumbnail_path)?;
|
||||
}
|
||||
|
||||
Ok(File::open(thumbnail_path)?)
|
||||
}
|
@ -7,9 +7,12 @@ use clap::Parser;
|
||||
#[command(version, about, long_about = None)]
|
||||
pub struct Cli {
|
||||
/// Address to listen on
|
||||
#[arg(short, long, default_value = "[::1]:3000")]
|
||||
#[arg(short, long, env, default_value = "[::1]:3000")]
|
||||
pub listen_address: String,
|
||||
/// Cache path ($TMP cascades through $XDG_CACHE_HOME, $TMPDIR and /tmp)
|
||||
#[arg(short, long, env, default_value = "$TMP/little-hesinde")]
|
||||
pub cache_path: String,
|
||||
/// Calibre library path
|
||||
#[arg(last = true)]
|
||||
#[arg(env, last = true)]
|
||||
pub library_path: String,
|
||||
}
|
||||
|
@ -1,12 +1,14 @@
|
||||
//! Configuration data.
|
||||
|
||||
use std::{
|
||||
env, fs, io,
|
||||
net::SocketAddr,
|
||||
net::ToSocketAddrs,
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
use thiserror::Error;
|
||||
use tracing::info;
|
||||
|
||||
use crate::cli::Cli;
|
||||
|
||||
@ -22,6 +24,9 @@ pub enum ConfigError {
|
||||
/// Error converting a string to a listening address.
|
||||
#[error("failed to convert into listening address")]
|
||||
ListeningAddressError(String),
|
||||
/// Error accessing the configured cache path.
|
||||
#[error("failed to access cache path")]
|
||||
CachePathError(#[from] io::Error),
|
||||
}
|
||||
|
||||
/// Application configuration.
|
||||
@ -33,6 +38,8 @@ pub struct Config {
|
||||
pub metadata_path: PathBuf,
|
||||
/// Address to listen on.
|
||||
pub listen_address: SocketAddr,
|
||||
/// Path to data like thumbnails.
|
||||
pub cache_path: PathBuf,
|
||||
}
|
||||
|
||||
impl Config {
|
||||
@ -69,10 +76,21 @@ impl Config {
|
||||
args.listen_address.clone(),
|
||||
))?;
|
||||
|
||||
let cache_path = if args.cache_path.starts_with("$TMP") {
|
||||
let cache_base = env::var("XDG_CACHE_HOME")
|
||||
.unwrap_or_else(|_| env::var("TMPDIR").unwrap_or("/tmp/".to_string()));
|
||||
PathBuf::from(&cache_base).join("little-hesinde")
|
||||
} else {
|
||||
PathBuf::from(&args.cache_path)
|
||||
};
|
||||
fs::create_dir_all(&cache_path)?;
|
||||
info!("Using {} for cache", cache_path.to_string_lossy());
|
||||
|
||||
Ok(Self {
|
||||
library_path,
|
||||
metadata_path,
|
||||
listen_address,
|
||||
cache_path,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -1,24 +1,74 @@
|
||||
//! Handle requests for cover images.
|
||||
|
||||
use std::sync::Arc;
|
||||
use std::{fs::File, path::Path as FilePath, sync::Arc};
|
||||
|
||||
use crate::{app_state::AppState, handlers::error::HandlerError};
|
||||
use crate::{
|
||||
app_state::AppState,
|
||||
cache::{self, CacheError},
|
||||
config::Config,
|
||||
handlers::error::HandlerError,
|
||||
};
|
||||
use calibre_db::calibre::Calibre;
|
||||
use poem::{
|
||||
error::NotFoundError,
|
||||
handler,
|
||||
web::{headers::ContentType, Data, Path},
|
||||
Response,
|
||||
};
|
||||
use tokio::fs::File;
|
||||
use thiserror::Error;
|
||||
use tokio::fs::File as AsyncFile;
|
||||
|
||||
/// Errors from fetching cover images.
|
||||
#[derive(Error, Debug)]
|
||||
pub enum CoverError {
|
||||
/// Error fetching a cover thumbnail.
|
||||
#[error("failed to access thumbnail")]
|
||||
ThumbnailError(#[from] CacheError),
|
||||
/// Error fetching a full cover.
|
||||
#[error("failed access cover")]
|
||||
FullCoverError(#[from] std::io::Error),
|
||||
}
|
||||
|
||||
/// Handle a request for the cover thumbnail of book with id `id`.
|
||||
#[handler]
|
||||
pub async fn handler_thumbnail(
|
||||
id: Path<u64>,
|
||||
state: Data<&Arc<AppState>>,
|
||||
) -> Result<Response, poem::Error> {
|
||||
cover(
|
||||
&state.calibre,
|
||||
&state.config,
|
||||
*id,
|
||||
|cover_path, cache_path| Ok(cache::get_thumbnail(cover_path, cache_path)?),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Handle a request for the cover image of book with id `id`.
|
||||
#[handler]
|
||||
pub async fn handler(id: Path<u64>, state: Data<&Arc<AppState>>) -> Result<Response, poem::Error> {
|
||||
let book = state
|
||||
.calibre
|
||||
.scalar_book(*id)
|
||||
.map_err(HandlerError::DataError)?;
|
||||
let cover_path = state.config.library_path.join(book.path).join("cover.jpg");
|
||||
let mut cover = File::open(cover_path).await.map_err(|_| NotFoundError)?;
|
||||
pub async fn handler_full(
|
||||
id: Path<u64>,
|
||||
state: Data<&Arc<AppState>>,
|
||||
) -> Result<Response, poem::Error> {
|
||||
cover(&state.calibre, &state.config, *id, |cover_path, _| {
|
||||
Ok(File::open(cover_path)?)
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
async fn cover<F>(
|
||||
calibre: &Calibre,
|
||||
config: &Config,
|
||||
id: u64,
|
||||
f: F,
|
||||
) -> Result<Response, poem::Error>
|
||||
where
|
||||
F: Fn(&FilePath, &FilePath) -> Result<File, CoverError>,
|
||||
{
|
||||
let book = calibre.scalar_book(id).map_err(HandlerError::DataError)?;
|
||||
let cover_path = config.library_path.join(book.path).join("cover.jpg");
|
||||
|
||||
let cover = f(&cover_path, &config.cache_path).map_err(|_| NotFoundError)?;
|
||||
let cover = AsyncFile::from_std(cover);
|
||||
crate::handlers::download::handler("cover.jpg", cover, &ContentType::jpeg().to_string()).await
|
||||
}
|
||||
|
@ -16,6 +16,7 @@ use tokio::signal;
|
||||
use tracing::info;
|
||||
|
||||
pub mod app_state;
|
||||
pub mod cache;
|
||||
pub mod cli;
|
||||
pub mod config;
|
||||
/// Data structs and their functions.
|
||||
@ -76,7 +77,7 @@ pub mod opds {
|
||||
pub mod templates;
|
||||
|
||||
pub const APP_NAME: &str = "little-hesinde";
|
||||
pub const VERSION: &str = "0.2.3";
|
||||
pub const VERSION: &str = "0.3.0";
|
||||
|
||||
/// Internal marker data in lieu of a proper `Accept` header.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
@ -116,7 +117,11 @@ pub async fn run(config: Config) -> Result<(), std::io::Error> {
|
||||
"/authors/:cursor/:sort_order",
|
||||
get(handlers::authors::handler),
|
||||
)
|
||||
.at("/cover/:id", get(handlers::cover::handler))
|
||||
.at("/cover/:id", get(handlers::cover::handler_full))
|
||||
.at(
|
||||
"/cover/:id/thumbnail",
|
||||
get(handlers::cover::handler_thumbnail),
|
||||
)
|
||||
.at("/book/:id/:format", get(handlers::books::handler_download))
|
||||
.at("/archive", get(handlers::source_archive::handler))
|
||||
.at("/search", get(handlers::search::handler))
|
||||
@ -142,7 +147,7 @@ pub async fn run(config: Config) -> Result<(), std::io::Error> {
|
||||
.with(Tracing);
|
||||
|
||||
let server = Server::new(TcpListener::bind(config.listen_address))
|
||||
.name("cops-web")
|
||||
.name("little-hesinde")
|
||||
.run(app);
|
||||
|
||||
tokio::select! {
|
||||
|
@ -14,7 +14,7 @@
|
||||
{% endif %}
|
||||
</hgroup>
|
||||
</header>
|
||||
<img class="cover" src="/cover/{{ book.data.id }}" alt="book cover">
|
||||
<img class="cover" src="/cover/{{ book.data.id }}/thumbnail" alt="book cover">
|
||||
<footer>
|
||||
<form>
|
||||
<fieldset role="group">
|
||||
|
Loading…
Reference in New Issue
Block a user