about summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--Cargo.lock238
-rw-r--r--Cargo.toml5
-rw-r--r--src/database/file/mod.rs173
-rw-r--r--src/database/mod.rs67
4 files changed, 399 insertions, 84 deletions
diff --git a/Cargo.lock b/Cargo.lock
index fecebc8..b6ce993 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -64,11 +64,20 @@ dependencies = [
 
 [[package]]
 name = "aho-corasick"
+version = "0.5.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ca972c2ea5f742bfce5687b9aef75506a764f61d37f8f649047846a9686ddb66"
+dependencies = [
+ "memchr 0.1.11",
+]
+
+[[package]]
+name = "aho-corasick"
 version = "0.7.18"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "1e37cfd5e7657ada45f742d6e99ca5788580b5c529dc78faf11ece6dc702656f"
 dependencies = [
- "memchr",
+ "memchr 2.4.0",
 ]
 
 [[package]]
@@ -174,7 +183,7 @@ dependencies = [
  "http-types",
  "httparse",
  "lazy_static",
- "log",
+ "log 0.4.14",
  "pin-project",
 ]
 
@@ -187,14 +196,14 @@ dependencies = [
  "concurrent-queue",
  "futures-lite",
  "libc",
- "log",
+ "log 0.4.14",
  "once_cell",
  "parking",
  "polling",
  "slab",
  "socket2",
  "waker-fn",
- "winapi",
+ "winapi 0.3.9",
 ]
 
 [[package]]
@@ -229,7 +238,7 @@ dependencies = [
  "libc",
  "once_cell",
  "signal-hook",
- "winapi",
+ "winapi 0.3.9",
 ]
 
 [[package]]
@@ -262,8 +271,8 @@ dependencies = [
  "async-channel",
  "async-std",
  "http-types",
- "log",
- "memchr",
+ "log 0.4.14",
+ "memchr 2.4.0",
  "pin-project-lite 0.1.12",
 ]
 
@@ -286,8 +295,8 @@ dependencies = [
  "futures-lite",
  "gloo-timers",
  "kv-log-macro",
- "log",
- "memchr",
+ "log 0.4.14",
+ "memchr 2.4.0",
  "num_cpus",
  "once_cell",
  "pin-project-lite 0.2.7",
@@ -310,7 +319,7 @@ checksum = "ba5fa6ed76cb2aa820707b4eb9ec46f42da9ce70b0eafab5e5e34942b38a44d5"
 dependencies = [
  "libc",
  "wasm-bindgen",
- "winapi",
+ "winapi 0.3.9",
 ]
 
 [[package]]
@@ -351,7 +360,7 @@ checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8"
 dependencies = [
  "hermit-abi",
  "libc",
- "winapi",
+ "winapi 0.3.9",
 ]
 
 [[package]]
@@ -494,7 +503,7 @@ dependencies = [
  "num-traits",
  "serde",
  "time 0.1.44",
- "winapi",
+ "winapi 0.3.9",
 ]
 
 [[package]]
@@ -514,7 +523,7 @@ checksum = "b3616f750b84d8f0de8a58bda93e08e2a81ad3f523089b05f1dffecab48c6cbd"
 dependencies = [
  "atty",
  "lazy_static",
- "winapi",
+ "winapi 0.3.9",
 ]
 
 [[package]]
@@ -525,7 +534,7 @@ checksum = "a2d47c1b11006b87e492b53b313bb699ce60e16613c4dddaa91f8f7c220ab2fa"
 dependencies = [
  "bytes",
  "futures-util",
- "memchr",
+ "memchr 2.4.0",
  "pin-project-lite 0.2.7",
  "tokio",
 ]
@@ -781,7 +790,7 @@ checksum = "18a857bc01b5ae04874234f6b5d16b8b8fa86910aa5777479c2669b5df607fce"
 dependencies = [
  "html5ever",
  "kuchiki",
- "regex",
+ "regex 1.5.4",
 ]
 
 [[package]]
@@ -804,14 +813,24 @@ dependencies = [
 
 [[package]]
 name = "env_logger"
+version = "0.3.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "15abd780e45b3ea4f76b4e9a26ff4843258dd8a3eed2775a0e7368c2e7936c2f"
+dependencies = [
+ "log 0.3.9",
+ "regex 0.1.80",
+]
+
+[[package]]
+name = "env_logger"
 version = "0.8.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "a19187fea3ac7e84da7dacf48de0c45d63c6a76f9490dae389aead16c243fce3"
 dependencies = [
  "atty",
  "humantime",
- "log",
- "regex",
+ "log 0.4.14",
+ "regex 1.5.4",
  "termcolor",
 ]
 
@@ -831,6 +850,17 @@ dependencies = [
 ]
 
 [[package]]
+name = "fd-lock"
+version = "3.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b8806dd91a06a7a403a8e596f9bfbfb34e469efbc363fc9c9713e79e26472e36"
+dependencies = [
+ "cfg-if 1.0.0",
+ "libc",
+ "winapi 0.3.9",
+]
+
+[[package]]
 name = "femme"
 version = "2.1.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -838,7 +868,7 @@ checksum = "2af1a24f391a5a94d756db5092c6576aad494b88a71a5a36b20c67b63e0df034"
 dependencies = [
  "cfg-if 0.1.10",
  "js-sys",
- "log",
+ "log 0.4.14",
  "serde",
  "serde_derive",
  "serde_json",
@@ -947,7 +977,7 @@ dependencies = [
  "fastrand",
  "futures-core",
  "futures-io",
- "memchr",
+ "memchr 2.4.0",
  "parking",
  "pin-project-lite 0.2.7",
  "waker-fn",
@@ -997,7 +1027,7 @@ dependencies = [
  "futures-macro",
  "futures-sink",
  "futures-task",
- "memchr",
+ "memchr 2.4.0",
  "pin-project-lite 0.2.7",
  "pin-utils",
  "proc-macro-hack",
@@ -1120,7 +1150,7 @@ version = "0.25.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "aafcf38a1a36118242d29b92e1b08ef84e67e4a5ed06e0a80be20e6a32bfed6b"
 dependencies = [
- "log",
+ "log 0.4.14",
  "mac",
  "markup5ever",
  "proc-macro2",
@@ -1143,7 +1173,7 @@ dependencies = [
  "deadpool",
  "futures",
  "http-types",
- "log",
+ "log 0.4.14",
  "rustls",
 ]
 
@@ -1223,6 +1253,16 @@ dependencies = [
 ]
 
 [[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 = "kittybox"
 version = "0.1.0"
 dependencies = [
@@ -1232,18 +1272,20 @@ dependencies = [
  "chrono",
  "easy-scraper",
  "ellipse",
- "env_logger",
+ "env_logger 0.8.4",
+ "fd-lock",
  "futures",
  "futures-util",
  "http-types",
  "lazy_static",
- "log",
+ "log 0.4.14",
  "markdown",
  "markup",
  "mobc",
  "mobc-redis",
  "mockito",
  "newbase60",
+ "paste",
  "prometheus",
  "retainer",
  "serde",
@@ -1251,6 +1293,7 @@ dependencies = [
  "serde_urlencoded",
  "surf",
  "tempdir",
+ "test-logger",
  "tide",
  "tide-testing",
  "url",
@@ -1274,7 +1317,7 @@ version = "1.0.7"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "0de8b303297635ad57c9f5059fd9cee7a47f8e8daa09df0fcd07dd39fb22977f"
 dependencies = [
- "log",
+ "log 0.4.14",
 ]
 
 [[package]]
@@ -1313,6 +1356,15 @@ dependencies = [
 
 [[package]]
 name = "log"
+version = "0.3.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e19e8d5c34a3e0e2223db8e060f9e8264aeeb5c5fc64a4ee9965c062211c024b"
+dependencies = [
+ "log 0.4.14",
+]
+
+[[package]]
+name = "log"
 version = "0.4.14"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "51b9bbe6c47d51fc3e1a9b945965946b4c44142ab8792c50835a980d362c2710"
@@ -1335,7 +1387,7 @@ checksum = "ef3aab6a1d529b112695f72beec5ee80e729cb45af58663ec902c8fac764ecdd"
 dependencies = [
  "lazy_static",
  "pipeline",
- "regex",
+ "regex 1.5.4",
 ]
 
 [[package]]
@@ -1365,7 +1417,7 @@ version = "0.10.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "a24f40fb03852d1cdd84330cddcaf98e9ec08a7b7768e952fad3b4cf048ec8fd"
 dependencies = [
- "log",
+ "log 0.4.14",
  "phf",
  "phf_codegen",
  "string_cache",
@@ -1381,6 +1433,15 @@ checksum = "7ffc5c5338469d4d3ea17d269fa8ea3512ad247247c30bd2df69e68309ed0a08"
 
 [[package]]
 name = "memchr"
+version = "0.1.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d8b629fb514376c675b98c1421e80b151d3817ac42d7c667717d282761418d20"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "memchr"
 version = "2.4.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "b16bd47d9e329435e309c58469fe0791c2d0d1ba96ec0954152a5ae2b04387dc"
@@ -1423,7 +1484,7 @@ dependencies = [
  "futures-core",
  "futures-timer",
  "futures-util",
- "log",
+ "log 0.4.14",
  "tokio",
 ]
 
@@ -1448,9 +1509,9 @@ dependencies = [
  "difference",
  "httparse",
  "lazy_static",
- "log",
+ "log 0.4.14",
  "rand 0.8.4",
- "regex",
+ "regex 1.5.4",
  "serde_json",
  "serde_urlencoded",
 ]
@@ -1480,7 +1541,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "ffb4262d26ed83a1c0a33a38fe2bb15797329c85770da05e6b828ddb782627af"
 dependencies = [
  "lexical-core",
- "memchr",
+ "memchr 2.4.0",
  "version_check",
 ]
 
@@ -1553,10 +1614,16 @@ dependencies = [
  "libc",
  "redox_syscall",
  "smallvec",
- "winapi",
+ "winapi 0.3.9",
 ]
 
 [[package]]
+name = "paste"
+version = "1.0.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "acbf547ad0c65e31259204bd90935776d1c693cec2f4ff7abb7a1bbbd40dfe58"
+
+[[package]]
 name = "percent-encoding"
 version = "2.1.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1668,9 +1735,9 @@ checksum = "92341d779fa34ea8437ef4d82d440d5e1ce3f3ff7f824aa64424cd481f9a1f25"
 dependencies = [
  "cfg-if 1.0.0",
  "libc",
- "log",
+ "log 0.4.14",
  "wepoll-ffi",
- "winapi",
+ "winapi 0.3.9",
 ]
 
 [[package]]
@@ -1741,7 +1808,7 @@ dependencies = [
  "fnv",
  "lazy_static",
  "libc",
- "memchr",
+ "memchr 2.4.0",
  "parking_lot",
  "procfs",
  "protobuf",
@@ -1773,7 +1840,7 @@ dependencies = [
  "libc",
  "rand_core 0.3.1",
  "rdrand",
- "winapi",
+ "winapi 0.3.9",
 ]
 
 [[package]]
@@ -1923,17 +1990,36 @@ dependencies = [
 
 [[package]]
 name = "regex"
+version = "0.1.80"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4fd4ace6a8cf7860714a2c2280d6c1f7e6a413486c13298bbc86fd3da019402f"
+dependencies = [
+ "aho-corasick 0.5.3",
+ "memchr 0.1.11",
+ "regex-syntax 0.3.9",
+ "thread_local",
+ "utf8-ranges",
+]
+
+[[package]]
+name = "regex"
 version = "1.5.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "d07a8629359eb56f1e2fb1652bb04212c072a87ba68546a04065d525673ac461"
 dependencies = [
- "aho-corasick",
- "memchr",
- "regex-syntax",
+ "aho-corasick 0.7.18",
+ "memchr 2.4.0",
+ "regex-syntax 0.6.25",
 ]
 
 [[package]]
 name = "regex-syntax"
+version = "0.3.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f9ec002c35e86791825ed294b50008eea9ddfc8def4420124fbc6b08db834957"
+
+[[package]]
+name = "regex-syntax"
 version = "0.6.25"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b"
@@ -1944,7 +2030,7 @@ version = "0.5.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7"
 dependencies = [
- "winapi",
+ "winapi 0.3.9",
 ]
 
 [[package]]
@@ -1955,7 +2041,7 @@ checksum = "59039dbf4a344af919780e9acdf7f9ce95deffb0152a72eca94b89d6a2bf66c0"
 dependencies = [
  "async-lock",
  "async-timer",
- "log",
+ "log 0.4.14",
  "rand 0.8.4",
 ]
 
@@ -1971,7 +2057,7 @@ dependencies = [
  "spin",
  "untrusted",
  "web-sys",
- "winapi",
+ "winapi 0.3.9",
 ]
 
 [[package]]
@@ -1996,7 +2082,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "5d1126dcf58e93cee7d098dbda643b5f92ed724f1f6a63007c1116eed6700c81"
 dependencies = [
  "base64 0.12.3",
- "log",
+ "log 0.4.14",
  "ring",
  "sct",
  "webpki",
@@ -2034,7 +2120,7 @@ dependencies = [
  "cssparser",
  "derive_more",
  "fxhash",
- "log",
+ "log 0.4.14",
  "matches",
  "phf",
  "phf_codegen",
@@ -2196,7 +2282,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "9e3dfc207c526015c632472a77be09cf1b6e46866581aecae5cc38fb4235dea2"
 dependencies = [
  "libc",
- "winapi",
+ "winapi 0.3.9",
 ]
 
 [[package]]
@@ -2319,7 +2405,7 @@ dependencies = [
  "futures-util",
  "http-client",
  "http-types",
- "log",
+ "log 0.4.14",
  "mime_guess",
  "pin-project-lite 0.2.7",
  "serde",
@@ -2375,6 +2461,15 @@ dependencies = [
 ]
 
 [[package]]
+name = "test-logger"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2e55ec868b79cb8e63f8921843c10e3083137cfaa171a67209e6a2656ccd4d8a"
+dependencies = [
+ "env_logger 0.3.5",
+]
+
+[[package]]
 name = "thin-slice"
 version = "0.1.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2401,6 +2496,25 @@ dependencies = [
 ]
 
 [[package]]
+name = "thread-id"
+version = "2.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a9539db560102d1cef46b8b78ce737ff0bb64e7e18d35b2a5688f7d097d0ff03"
+dependencies = [
+ "kernel32-sys",
+ "libc",
+]
+
+[[package]]
+name = "thread_local"
+version = "0.2.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8576dbbfcaef9641452d5cf0df9b0e7eeab7694956dd33bb61515fb8f18cfdd5"
+dependencies = [
+ "thread-id",
+]
+
+[[package]]
 name = "tide"
 version = "0.16.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2416,7 +2530,7 @@ dependencies = [
  "http-client",
  "http-types",
  "kv-log-macro",
- "log",
+ "log 0.4.14",
  "pin-project-lite 0.2.7",
  "route-recognizer",
  "serde",
@@ -2443,7 +2557,7 @@ checksum = "6db9e6914ab8b1ae1c260a4ae7a49b6c5611b40328a735b21862567685e73255"
 dependencies = [
  "libc",
  "wasi 0.10.0+wasi-snapshot-preview1",
- "winapi",
+ "winapi 0.3.9",
 ]
 
 [[package]]
@@ -2458,7 +2572,7 @@ dependencies = [
  "stdweb",
  "time-macros",
  "version_check",
- "winapi",
+ "winapi 0.3.9",
 ]
 
 [[package]]
@@ -2507,7 +2621,7 @@ checksum = "98c8b05dc14c75ea83d63dd391100353789f5f24b8b3866542a5e85c8be8e985"
 dependencies = [
  "autocfg",
  "bytes",
- "memchr",
+ "memchr 2.4.0",
  "num_cpus",
  "pin-project-lite 0.2.7",
 ]
@@ -2521,7 +2635,7 @@ dependencies = [
  "bytes",
  "futures-core",
  "futures-sink",
- "log",
+ "log 0.4.14",
  "pin-project-lite 0.2.7",
  "tokio",
 ]
@@ -2607,6 +2721,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9"
 
 [[package]]
+name = "utf8-ranges"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a1ca13c08c41c9c3e04224ed9ff80461d97e121589ff27c753a16cb10830ae0f"
+
+[[package]]
 name = "value-bag"
 version = "1.0.0-alpha.7"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2661,7 +2781,7 @@ checksum = "3b33f6a0694ccfea53d94db8b2ed1c3a8a4c86dd936b13b9f0a15ec4a451b900"
 dependencies = [
  "bumpalo",
  "lazy_static",
- "log",
+ "log 0.4.14",
  "proc-macro2",
  "quote",
  "syn",
@@ -2749,6 +2869,12 @@ dependencies = [
 
 [[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.9"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
@@ -2758,6 +2884,12 @@ dependencies = [
 ]
 
 [[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"
@@ -2769,7 +2901,7 @@ version = "0.1.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178"
 dependencies = [
- "winapi",
+ "winapi 0.3.9",
 ]
 
 [[package]]
diff --git a/Cargo.toml b/Cargo.toml
index 73ee6ed..3974d9c 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -23,12 +23,15 @@ required-features = ["util"]
 tide-testing = "^0.1.3"      # tide testing helper
 mockito = "^0.30.0"          # HTTP mocking for Rust.
 tempdir = "^0.3.7"           # A library for managing a temporary directory and deleting all contents when it's dropped
+paste = "^1.0.5"             # Macros for all your token pasting needs
+test-logger = "^0.1.0"       # Simple helper to initialize env_logger before unit and integration tests
 
 [dependencies]
 async-trait = "^0.1.50"      # Type erasure for async trait methods
 easy-scraper = "^0.2.0"      # HTML scraping library focused on ease of use
 ellipse = "^0.2.0"           # Truncate and ellipsize strings in a human-friendly way
 env_logger = "^0.8.3"        # A logging implementation for `log` which is configured via an environment variable
+fd-lock = "^3.0.0"           # Advisory reader-writer locks for files
 futures = "^0.3.14"          # An implementation of futures and streams
 futures-util = "^0.3.14"     # Common utilities and extension traits for the futures-rs library
 http-types = "^2.11.0"       # Common types for HTTP operations
@@ -47,7 +50,7 @@ version = "^1.0.42"
 optional = true
 [dependencies.async-std]     # Async version of the Rust standard library
 version = "^1.9.0"
-features = ["attributes"]
+features = ["attributes", "unstable"]
 [dependencies.chrono]        # Date and time library for Rust
 version = "^0.4.19"
 features = ["serde"]
diff --git a/src/database/file/mod.rs b/src/database/file/mod.rs
new file mode 100644
index 0000000..36300fc
--- /dev/null
+++ b/src/database/file/mod.rs
@@ -0,0 +1,173 @@
+//pub mod async_file_ext;
+use async_std::fs::{File, OpenOptions};
+use async_std::io::{ErrorKind as IOErrorKind, BufReader};
+use async_std::io::prelude::*;
+use async_std::task::spawn_blocking;
+use async_trait::async_trait;
+use crate::database::{ErrorKind, Result, Storage, StorageError};
+use fd_lock::RwLock;
+use log::debug;
+use std::path::{Path, PathBuf};
+
+impl From<std::io::Error> for StorageError {
+    fn from(source: std::io::Error) -> Self {
+        Self::with_source(
+            match source.kind() {
+                IOErrorKind::NotFound => ErrorKind::NotFound,
+                _ => ErrorKind::Backend,
+            },
+            "file I/O error",
+            Box::new(source),
+        )
+    }
+}
+
+async fn get_lockable_file(file: File) -> RwLock<File> {
+    debug!("Trying to create a file lock");
+    spawn_blocking(move || RwLock::new(file)).await
+}
+
+fn url_to_path(root: &Path, url: &str) -> PathBuf {
+    let url = http_types::Url::parse(url).expect("Couldn't parse a URL");
+    let mut path: PathBuf = root.to_owned();
+    path.push(url.origin().ascii_serialization() + &url.path().to_string() + ".json");
+
+    path
+}
+
+#[derive(Clone)]
+pub struct FileStorage {
+    root_dir: PathBuf,
+}
+
+impl FileStorage {
+    pub async fn new(root_dir: PathBuf) -> Result<Self> {
+        // TODO check if the dir is writable
+        Ok(Self { root_dir })
+    }
+}
+
+#[async_trait]
+impl Storage for FileStorage {
+    async fn post_exists(&self, url: &str) -> Result<bool> {
+        let path = url_to_path(&self.root_dir, url);
+        debug!("Checking if {:?} exists...", path);
+        Ok(spawn_blocking(move || path.is_file()).await)
+    }
+
+    async fn get_post(&self, url: &str) -> Result<Option<serde_json::Value>> {
+        let path = url_to_path(&self.root_dir, url);
+        debug!("Opening {:?}", path);
+        // We have to special-case in here because the function should return Ok(None) on 404
+        match File::open(path).await {
+            Ok(f) => {
+                let lock = get_lockable_file(f).await;
+                let guard = lock.read()?;
+
+                // HOW DOES THIS TYPECHECK?!!!!!!!!
+                // Read::read(&mut self) requires a mutable reference
+                // yet Read is implemented for &File
+                // We can't get a &mut File from &File, can we?
+                // And we need a &mut File to use Read::read_to_string()
+                // Yet if we pass it to a BufReader it works?!!
+                //
+                // I hate magic
+                //
+                // TODO find a way to get rid of BufReader here
+                let mut content = String::new();
+                let mut reader = BufReader::new(&*guard);
+                reader.read_to_string(&mut content).await?;
+                drop(reader);
+                drop(guard);
+                Ok(Some(serde_json::from_str(&content)?))
+            }
+            Err(err) => {
+                if err.kind() == IOErrorKind::NotFound {
+                    Ok(None)
+                } else {
+                    Err(err.into())
+                }
+            }
+        }
+    }
+
+    async fn put_post<'a>(&self, post: &'a serde_json::Value, user: &'a str) -> Result<()> {
+        let key = post["properties"]["uid"][0]
+        .as_str()
+        .expect("Tried to save a post without UID");
+        let path = url_to_path(&self.root_dir, key);
+
+        debug!("Creating {:?}", path);
+
+        let parent = path.parent().unwrap().to_owned();
+        if !spawn_blocking(move || parent.is_dir()).await {
+            async_std::fs::create_dir_all(path.parent().unwrap()).await?;
+        }
+
+        let f = OpenOptions::new()
+            .write(true)
+            .create_new(true)
+            .open(&path)
+            .await?;
+        
+        let mut lock = get_lockable_file(f).await;
+        let mut guard = lock.write()?;
+
+        (*guard).write(post.to_string().as_bytes()).await?;
+        drop(guard);
+
+        if post["properties"]["url"].is_array() {
+            for url in post["properties"]["url"]
+                .as_array()
+                .unwrap()
+                .iter()
+                .map(|i| i.as_str().unwrap())
+            {
+                // TODO consider using the symlink crate
+                // to promote cross-platform compat on Windows
+                // do we even need to support Windows?...
+                if url != key && url.starts_with(user) {
+                    let link = url_to_path(&self.root_dir, url);
+                    debug!("Creating a symlink at {:?}", link);
+                    let orig = path.clone();
+                    spawn_blocking(move || { std::os::unix::fs::symlink(orig, link) }).await?;
+                }
+            }
+        }
+
+        Ok(())
+    }
+
+    async fn update_post<'a>(&self, url: &'a str, update: serde_json::Value) -> Result<()> {
+        todo!()
+    }
+
+    async fn get_channels(
+        &self,
+        user: &crate::indieauth::User,
+    ) -> Result<Vec<super::MicropubChannel>> {
+        todo!()
+    }
+
+    async fn read_feed_with_limit<'a>(
+        &self,
+        url: &'a str,
+        after: &'a Option<String>,
+        limit: usize,
+        user: &'a Option<String>,
+    ) -> Result<Option<serde_json::Value>> {
+        todo!()
+    }
+
+    async fn delete_post<'a>(&self, url: &'a str) -> Result<()> {
+        todo!()
+    }
+
+    async fn get_setting<'a>(&self, setting: &'a str, user: &'a str) -> Result<String> {
+        todo!()
+    }
+
+    async fn set_setting<'a>(&self, setting: &'a str, user: &'a str, value: &'a str) -> Result<()> {
+        todo!()
+    }
+}
diff --git a/src/database/mod.rs b/src/database/mod.rs
index 7b144f8..58f0a35 100644
--- a/src/database/mod.rs
+++ b/src/database/mod.rs
@@ -7,6 +7,8 @@ mod redis;
 pub use crate::database::redis::RedisStorage;
 #[cfg(test)]
 pub use redis::tests::{get_redis_instance, RedisInstance};
+mod file;
+pub use crate::database::file::FileStorage;
 
 #[derive(Serialize, Deserialize, PartialEq, Debug)]
 pub struct MicropubChannel {
@@ -133,12 +135,6 @@ pub trait Storage: Clone + Send + Sync {
     /// Note that the `post` object MUST have `post["properties"]["uid"][0]` defined.
     async fn put_post<'a>(&self, post: &'a serde_json::Value, user: &'a str) -> Result<()>;
 
-    /*/// Save a post and add it to the relevant feeds listed in `post["properties"]["channel"]`.
-    ///
-    /// Note that the `post` object MUST have `post["properties"]["uid"][0]` defined
-    /// and `post["properties"]["channel"]` defined, even if it's empty.
-    async fn put_and_index_post<'a>(&mut self, post: &'a serde_json::Value) -> Result<()>;*/
-
     /// Modify a post using an update object as defined in the Micropub spec.
     ///
     /// Note to implementors: the update operation MUST be atomic OR MUST lock the database
@@ -191,6 +187,7 @@ mod tests {
     use super::redis::tests::get_redis_instance;
     use super::{MicropubChannel, Storage};
     use serde_json::json;
+    use paste::paste;
 
     async fn test_backend_basic_operations<Backend: Storage>(backend: Backend) {
         let post: serde_json::Value = json!({
@@ -210,7 +207,7 @@ mod tests {
             .put_post(&post, "https://fireburn.ru/")
             .await
             .unwrap();
-        if let Ok(Some(returned_post)) = backend.get_post(&key).await {
+        if let Some(returned_post) = backend.get_post(&key).await.unwrap() {
             assert!(returned_post.is_object());
             assert_eq!(
                 returned_post["type"].as_array().unwrap().len(),
@@ -300,30 +297,40 @@ mod tests {
             "Vika's Hideout"
         );
     }
-
-    #[async_std::test]
-    async fn test_redis_storage_basic_operations() {
-        let redis_instance = get_redis_instance().await;
-        let backend = super::RedisStorage::new(redis_instance.uri().to_string())
-            .await
-            .unwrap();
-        test_backend_basic_operations(backend).await;
-    }
-    #[async_std::test]
-    async fn test_redis_storage_channel_list() {
-        let redis_instance = get_redis_instance().await;
-        let backend = super::RedisStorage::new(redis_instance.uri().to_string())
-            .await
-            .unwrap();
-        test_backend_get_channel_list(backend).await;
+    macro_rules! redis_test {
+        ($func_name:expr) => {
+            paste! {
+                #[async_std::test]
+                async fn [<redis_ $func_name>] () {
+                    test_logger::ensure_env_logger_initialized();
+                    let redis_instance = get_redis_instance().await;
+                    let backend = super::RedisStorage::new(redis_instance.uri().to_string())
+                        .await
+                        .unwrap();
+                    $func_name(backend).await
+                }
+            }
+        }
     }
 
-    #[async_std::test]
-    async fn test_redis_settings() {
-        let redis_instance = get_redis_instance().await;
-        let backend = super::RedisStorage::new(redis_instance.uri().to_string())
-            .await
-            .unwrap();
-        test_backend_settings(backend).await;
+    macro_rules! file_test {
+        ($func_name:expr) => {
+            paste! {
+                #[async_std::test]
+                async fn [<file_ $func_name>] () {
+                    test_logger::ensure_env_logger_initialized();
+                    let tempdir = tempdir::TempDir::new("file").expect("Failed to create tempdir");
+                    let backend = super::FileStorage::new(tempdir.into_path()).await.unwrap();
+                    $func_name(backend).await
+                }
+            }
+        }
     }
+
+    redis_test!(test_backend_basic_operations);
+    redis_test!(test_backend_get_channel_list);
+    redis_test!(test_backend_settings);
+    file_test!(test_backend_basic_operations);
+    file_test!(test_backend_get_channel_list);
+    file_test!(test_backend_settings);
 }