Merge pull request 'Support for storing media on S3' (#1149) from lx/Plume:s3 into main
Reviewed-on: https://git.joinplu.me/Plume/Plume/pulls/1149 Reviewed-by: trinity-1686a <trinity-1686a@noreply@joinplu.me>
This commit is contained in:
		
						commit
						304fb740d8
					
				
							
								
								
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @ -20,3 +20,4 @@ search_index | ||||
| __pycache__ | ||||
| .vscode/ | ||||
| *-journal | ||||
| .direnv/ | ||||
|  | ||||
							
								
								
									
										259
									
								
								Cargo.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										259
									
								
								Cargo.lock
									
									
									
										generated
									
									
									
								
							| @ -227,7 +227,7 @@ dependencies = [ | ||||
|  "derive_builder", | ||||
|  "diligent-date-parser", | ||||
|  "never", | ||||
|  "quick-xml", | ||||
|  "quick-xml 0.27.1", | ||||
| ] | ||||
| 
 | ||||
| [[package]] | ||||
| @ -241,6 +241,20 @@ dependencies = [ | ||||
|  "winapi 0.3.9", | ||||
| ] | ||||
| 
 | ||||
| [[package]] | ||||
| name = "attohttpc" | ||||
| version = "0.22.0" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "1fcf00bc6d5abb29b5f97e3c61a90b6d3caa12f3faf897d4a3e3607c050a35a7" | ||||
| dependencies = [ | ||||
|  "http 0.2.8", | ||||
|  "log 0.4.17", | ||||
|  "native-tls", | ||||
|  "serde 1.0.152", | ||||
|  "serde_json", | ||||
|  "url 2.3.1", | ||||
| ] | ||||
| 
 | ||||
| [[package]] | ||||
| name = "atty" | ||||
| version = "0.2.14" | ||||
| @ -267,6 +281,32 @@ version = "1.1.0" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" | ||||
| 
 | ||||
| [[package]] | ||||
| name = "aws-creds" | ||||
| version = "0.34.1" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "3776743bb68d4ad02ba30ba8f64373f1be4e082fe47651767171ce75bb2f6cf5" | ||||
| dependencies = [ | ||||
|  "attohttpc", | ||||
|  "dirs", | ||||
|  "log 0.4.17", | ||||
|  "quick-xml 0.26.0", | ||||
|  "rust-ini 0.18.0", | ||||
|  "serde 1.0.152", | ||||
|  "thiserror", | ||||
|  "time 0.3.17", | ||||
|  "url 2.3.1", | ||||
| ] | ||||
| 
 | ||||
| [[package]] | ||||
| name = "aws-region" | ||||
| version = "0.25.3" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "056557a61427d0e5ba29dd931031c8ffed4ee7a550e7cd55692a9d8deb0a9dba" | ||||
| dependencies = [ | ||||
|  "thiserror", | ||||
| ] | ||||
| 
 | ||||
| [[package]] | ||||
| name = "backtrace" | ||||
| version = "0.1.8" | ||||
| @ -389,6 +429,25 @@ dependencies = [ | ||||
|  "generic-array", | ||||
| ] | ||||
| 
 | ||||
| [[package]] | ||||
| name = "block-buffer" | ||||
| version = "0.10.4" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" | ||||
| dependencies = [ | ||||
|  "generic-array", | ||||
| ] | ||||
| 
 | ||||
| [[package]] | ||||
| name = "block_on_proc" | ||||
| version = "0.2.0" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "b872f3528eeeb4370ee73b51194dc1cd93680c2d0eb6c7a223889038d2c1a167" | ||||
| dependencies = [ | ||||
|  "quote 1.0.23", | ||||
|  "syn 1.0.107", | ||||
| ] | ||||
| 
 | ||||
| [[package]] | ||||
| name = "blowfish" | ||||
| version = "0.9.1" | ||||
| @ -578,7 +637,7 @@ checksum = "19b076e143e1d9538dde65da30f8481c2a6c44040edb8e02b9bf1351edb92ce3" | ||||
| dependencies = [ | ||||
|  "lazy_static", | ||||
|  "nom 5.1.2", | ||||
|  "rust-ini", | ||||
|  "rust-ini 0.13.0", | ||||
|  "serde 1.0.152", | ||||
|  "serde-hjson", | ||||
|  "serde_json", | ||||
| @ -636,10 +695,10 @@ dependencies = [ | ||||
|  "aes-gcm", | ||||
|  "base64 0.13.1", | ||||
|  "hkdf", | ||||
|  "hmac", | ||||
|  "hmac 0.10.1", | ||||
|  "percent-encoding 2.2.0", | ||||
|  "rand 0.8.5", | ||||
|  "sha2", | ||||
|  "sha2 0.9.9", | ||||
|  "time 0.1.45", | ||||
| ] | ||||
| 
 | ||||
| @ -1141,6 +1200,17 @@ dependencies = [ | ||||
|  "generic-array", | ||||
| ] | ||||
| 
 | ||||
| [[package]] | ||||
| name = "digest" | ||||
| version = "0.10.6" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "8168378f4e5023e7218c89c891c0fd8ecdb5e5e4f18cb78f38cf245dd021e76f" | ||||
| dependencies = [ | ||||
|  "block-buffer 0.10.4", | ||||
|  "crypto-common", | ||||
|  "subtle", | ||||
| ] | ||||
| 
 | ||||
| [[package]] | ||||
| name = "diligent-date-parser" | ||||
| version = "0.1.4" | ||||
| @ -1150,6 +1220,32 @@ dependencies = [ | ||||
|  "chrono", | ||||
| ] | ||||
| 
 | ||||
| [[package]] | ||||
| name = "dirs" | ||||
| version = "4.0.0" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "ca3aa72a6f96ea37bbc5aa912f6788242832f75369bdfdadcb0e38423f100059" | ||||
| dependencies = [ | ||||
|  "dirs-sys", | ||||
| ] | ||||
| 
 | ||||
| [[package]] | ||||
| name = "dirs-sys" | ||||
| version = "0.3.7" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "1b1d1d91c932ef41c0f2663aa8b0ca0342d444d842c06914aa0a7e352d0bada6" | ||||
| dependencies = [ | ||||
|  "libc", | ||||
|  "redox_users", | ||||
|  "winapi 0.3.9", | ||||
| ] | ||||
| 
 | ||||
| [[package]] | ||||
| name = "dlv-list" | ||||
| version = "0.3.0" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "0688c2a7f92e427f44895cd63841bff7b29f8d7a1648b9e7e07a4a365b2e1257" | ||||
| 
 | ||||
| [[package]] | ||||
| name = "dotenv" | ||||
| version = "0.15.0" | ||||
| @ -1793,8 +1889,8 @@ version = "0.10.0" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "51ab2f639c231793c5f6114bdb9bbe50a7dbbfcd7c7c6bd8475dec2d991e964f" | ||||
| dependencies = [ | ||||
|  "digest", | ||||
|  "hmac", | ||||
|  "digest 0.9.0", | ||||
|  "hmac 0.10.1", | ||||
| ] | ||||
| 
 | ||||
| [[package]] | ||||
| @ -1804,7 +1900,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "c1441c6b1e930e2817404b5046f1f989899143a12bf92de603b69f4e0aee1e15" | ||||
| dependencies = [ | ||||
|  "crypto-mac", | ||||
|  "digest", | ||||
|  "digest 0.9.0", | ||||
| ] | ||||
| 
 | ||||
| [[package]] | ||||
| name = "hmac" | ||||
| version = "0.12.1" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" | ||||
| dependencies = [ | ||||
|  "digest 0.10.6", | ||||
| ] | ||||
| 
 | ||||
| [[package]] | ||||
| @ -2549,6 +2654,17 @@ version = "0.1.9" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "a3e378b66a060d48947b590737b30a1be76706c8dd7b8ba0f2fe3989c68a853f" | ||||
| 
 | ||||
| [[package]] | ||||
| name = "maybe-async" | ||||
| version = "0.2.7" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "0f1b8c13cb1f814b634a96b2c725449fe7ed464a7b8781de8688be5ffbd3f305" | ||||
| dependencies = [ | ||||
|  "proc-macro2 1.0.49", | ||||
|  "quote 1.0.23", | ||||
|  "syn 1.0.107", | ||||
| ] | ||||
| 
 | ||||
| [[package]] | ||||
| name = "maybe-uninit" | ||||
| version = "2.0.0" | ||||
| @ -2641,6 +2757,15 @@ dependencies = [ | ||||
|  "unicase 2.6.0", | ||||
| ] | ||||
| 
 | ||||
| [[package]] | ||||
| name = "minidom" | ||||
| version = "0.15.1" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "2e9ce45d459e358790a285e7609ff5ae4cfab88b75f237e8838e62029dda397b" | ||||
| dependencies = [ | ||||
|  "rxml", | ||||
| ] | ||||
| 
 | ||||
| [[package]] | ||||
| name = "minimal-lexical" | ||||
| version = "0.2.1" | ||||
| @ -3063,6 +3188,16 @@ dependencies = [ | ||||
|  "vcpkg", | ||||
| ] | ||||
| 
 | ||||
| [[package]] | ||||
| name = "ordered-multimap" | ||||
| version = "0.4.3" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "ccd746e37177e1711c20dd619a1620f34f5c8b569c53590a72dedd5344d8924a" | ||||
| dependencies = [ | ||||
|  "dlv-list", | ||||
|  "hashbrown 0.12.3", | ||||
| ] | ||||
| 
 | ||||
| [[package]] | ||||
| name = "overload" | ||||
| version = "0.1.1" | ||||
| @ -3409,6 +3544,7 @@ dependencies = [ | ||||
|  "riker", | ||||
|  "rocket", | ||||
|  "rocket_i18n", | ||||
|  "rust-s3", | ||||
|  "scheduled-thread-pool", | ||||
|  "serde 1.0.152", | ||||
|  "serde_derive", | ||||
| @ -3552,6 +3688,16 @@ version = "1.2.3" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" | ||||
| 
 | ||||
| [[package]] | ||||
| name = "quick-xml" | ||||
| version = "0.26.0" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "7f50b1c63b38611e7d4d7f68b82d3ad0cc71a2ad2e7f61fc10f1328d917c93cd" | ||||
| dependencies = [ | ||||
|  "memchr", | ||||
|  "serde 1.0.152", | ||||
| ] | ||||
| 
 | ||||
| [[package]] | ||||
| name = "quick-xml" | ||||
| version = "0.27.1" | ||||
| @ -3836,6 +3982,17 @@ dependencies = [ | ||||
|  "bitflags 1.3.2", | ||||
| ] | ||||
| 
 | ||||
| [[package]] | ||||
| name = "redox_users" | ||||
| version = "0.4.3" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "b033d837a7cf162d7993aded9304e30a83213c648b6e389db233191f891e5c2b" | ||||
| dependencies = [ | ||||
|  "getrandom 0.2.8", | ||||
|  "redox_syscall 0.2.16", | ||||
|  "thiserror", | ||||
| ] | ||||
| 
 | ||||
| [[package]] | ||||
| name = "regex" | ||||
| version = "1.7.0" | ||||
| @ -3967,6 +4124,7 @@ dependencies = [ | ||||
|  "tokio 1.24.1", | ||||
|  "tokio-native-tls", | ||||
|  "tokio-socks", | ||||
|  "tokio-util 0.7.4", | ||||
|  "tower-service", | ||||
|  "url 2.3.1", | ||||
|  "wasm-bindgen", | ||||
| @ -4159,6 +4317,50 @@ version = "0.13.0" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "3e52c148ef37f8c375d49d5a73aa70713125b7f19095948a923f80afdeb22ec2" | ||||
| 
 | ||||
| [[package]] | ||||
| name = "rust-ini" | ||||
| version = "0.18.0" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "f6d5f2436026b4f6e79dc829837d467cc7e9a55ee40e750d716713540715a2df" | ||||
| dependencies = [ | ||||
|  "cfg-if 1.0.0", | ||||
|  "ordered-multimap", | ||||
| ] | ||||
| 
 | ||||
| [[package]] | ||||
| name = "rust-s3" | ||||
| version = "0.33.0" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "1b2ac5ff6acfbe74226fa701b5ef793aaa054055c13ebb7060ad36942956e027" | ||||
| dependencies = [ | ||||
|  "async-trait", | ||||
|  "aws-creds", | ||||
|  "aws-region", | ||||
|  "base64 0.13.1", | ||||
|  "block_on_proc", | ||||
|  "bytes 1.3.0", | ||||
|  "cfg-if 1.0.0", | ||||
|  "futures 0.3.25", | ||||
|  "hex", | ||||
|  "hmac 0.12.1", | ||||
|  "http 0.2.8", | ||||
|  "log 0.4.17", | ||||
|  "maybe-async", | ||||
|  "md5", | ||||
|  "minidom", | ||||
|  "percent-encoding 2.2.0", | ||||
|  "quick-xml 0.26.0", | ||||
|  "reqwest 0.11.13", | ||||
|  "serde 1.0.152", | ||||
|  "serde_derive", | ||||
|  "sha2 0.10.6", | ||||
|  "thiserror", | ||||
|  "time 0.3.17", | ||||
|  "tokio 1.24.1", | ||||
|  "tokio-stream", | ||||
|  "url 2.3.1", | ||||
| ] | ||||
| 
 | ||||
| [[package]] | ||||
| name = "rust-stemmers" | ||||
| version = "1.2.0" | ||||
| @ -4184,6 +4386,25 @@ dependencies = [ | ||||
|  "semver", | ||||
| ] | ||||
| 
 | ||||
| [[package]] | ||||
| name = "rxml" | ||||
| version = "0.8.2" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "1a071866b8c681dc2cfffa77184adc32b57b0caad4e620b6292609703bceb804" | ||||
| dependencies = [ | ||||
|  "bytes 1.3.0", | ||||
|  "pin-project-lite 0.2.9", | ||||
|  "rxml_validation", | ||||
|  "smartstring", | ||||
|  "tokio 1.24.1", | ||||
| ] | ||||
| 
 | ||||
| [[package]] | ||||
| name = "rxml_validation" | ||||
| version = "0.8.2" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "53bc79743f9a66c2fb1f951cd83735f275d46bfe466259fbc5897bb60a0d00ee" | ||||
| 
 | ||||
| [[package]] | ||||
| name = "ryu" | ||||
| version = "1.0.12" | ||||
| @ -4363,13 +4584,24 @@ version = "0.9.9" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "4d58a1e1bf39749807d89cf2d98ac2dfa0ff1cb3faa38fbb64dd88ac8013d800" | ||||
| dependencies = [ | ||||
|  "block-buffer", | ||||
|  "block-buffer 0.9.0", | ||||
|  "cfg-if 1.0.0", | ||||
|  "cpufeatures", | ||||
|  "digest", | ||||
|  "digest 0.9.0", | ||||
|  "opaque-debug", | ||||
| ] | ||||
| 
 | ||||
| [[package]] | ||||
| name = "sha2" | ||||
| version = "0.10.6" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "82e6b795fe2e3b1e845bafcb27aa35405c4d47cdfc92af5fc8d3002f76cebdc0" | ||||
| dependencies = [ | ||||
|  "cfg-if 1.0.0", | ||||
|  "cpufeatures", | ||||
|  "digest 0.10.6", | ||||
| ] | ||||
| 
 | ||||
| [[package]] | ||||
| name = "sharded-slab" | ||||
| version = "0.1.4" | ||||
| @ -4459,6 +4691,15 @@ version = "1.10.0" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0" | ||||
| 
 | ||||
| [[package]] | ||||
| name = "smartstring" | ||||
| version = "0.2.10" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "e714dff2b33f2321fdcd475b71cec79781a692d846f37f415fb395a1d2bcd48e" | ||||
| dependencies = [ | ||||
|  "static_assertions", | ||||
| ] | ||||
| 
 | ||||
| [[package]] | ||||
| name = "snap" | ||||
| version = "1.1.0" | ||||
|  | ||||
| @ -68,12 +68,13 @@ ructe = "0.15.0" | ||||
| rsass = "0.26" | ||||
| 
 | ||||
| [features] | ||||
| default = ["postgres"] | ||||
| default = ["postgres", "s3"] | ||||
| postgres = ["plume-models/postgres", "diesel/postgres"] | ||||
| sqlite = ["plume-models/sqlite", "diesel/sqlite"] | ||||
| debug-mailer = [] | ||||
| test = [] | ||||
| search-lindera = ["plume-models/search-lindera"] | ||||
| s3 = ["plume-models/s3"] | ||||
| 
 | ||||
| [workspace] | ||||
| members = ["plume-api", "plume-cli", "plume-models", "plume-common", "plume-front", "plume-macro"] | ||||
|  | ||||
							
								
								
									
										116
									
								
								flake.lock
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										116
									
								
								flake.lock
									
									
									
										generated
									
									
									
										Normal file
									
								
							| @ -0,0 +1,116 @@ | ||||
| { | ||||
|   "nodes": { | ||||
|     "flake-utils": { | ||||
|       "inputs": { | ||||
|         "systems": "systems" | ||||
|       }, | ||||
|       "locked": { | ||||
|         "lastModified": 1681202837, | ||||
|         "narHash": "sha256-H+Rh19JDwRtpVPAWp64F+rlEtxUWBAQW28eAi3SRSzg=", | ||||
|         "owner": "numtide", | ||||
|         "repo": "flake-utils", | ||||
|         "rev": "cfacdce06f30d2b68473a46042957675eebb3401", | ||||
|         "type": "github" | ||||
|       }, | ||||
|       "original": { | ||||
|         "owner": "numtide", | ||||
|         "repo": "flake-utils", | ||||
|         "type": "github" | ||||
|       } | ||||
|     }, | ||||
|     "flake-utils_2": { | ||||
|       "inputs": { | ||||
|         "systems": "systems_2" | ||||
|       }, | ||||
|       "locked": { | ||||
|         "lastModified": 1681202837, | ||||
|         "narHash": "sha256-H+Rh19JDwRtpVPAWp64F+rlEtxUWBAQW28eAi3SRSzg=", | ||||
|         "owner": "numtide", | ||||
|         "repo": "flake-utils", | ||||
|         "rev": "cfacdce06f30d2b68473a46042957675eebb3401", | ||||
|         "type": "github" | ||||
|       }, | ||||
|       "original": { | ||||
|         "owner": "numtide", | ||||
|         "repo": "flake-utils", | ||||
|         "type": "github" | ||||
|       } | ||||
|     }, | ||||
|     "nixpkgs": { | ||||
|       "locked": { | ||||
|         "lastModified": 1683408522, | ||||
|         "narHash": "sha256-9kcPh6Uxo17a3kK3XCHhcWiV1Yu1kYj22RHiymUhMkU=", | ||||
|         "owner": "NixOS", | ||||
|         "repo": "nixpkgs", | ||||
|         "rev": "897876e4c484f1e8f92009fd11b7d988a121a4e7", | ||||
|         "type": "github" | ||||
|       }, | ||||
|       "original": { | ||||
|         "owner": "NixOS", | ||||
|         "ref": "nixos-unstable", | ||||
|         "repo": "nixpkgs", | ||||
|         "type": "github" | ||||
|       } | ||||
|     }, | ||||
|     "root": { | ||||
|       "inputs": { | ||||
|         "flake-utils": "flake-utils", | ||||
|         "nixpkgs": "nixpkgs", | ||||
|         "rust-overlay": "rust-overlay" | ||||
|       } | ||||
|     }, | ||||
|     "rust-overlay": { | ||||
|       "inputs": { | ||||
|         "flake-utils": "flake-utils_2", | ||||
|         "nixpkgs": [ | ||||
|           "nixpkgs" | ||||
|         ] | ||||
|       }, | ||||
|       "locked": { | ||||
|         "lastModified": 1683857898, | ||||
|         "narHash": "sha256-pyVY4UxM6zUX97g6bk6UyCbZGCWZb2Zykrne8YxacRA=", | ||||
|         "owner": "oxalica", | ||||
|         "repo": "rust-overlay", | ||||
|         "rev": "4e7fba3f37f5e184ada0ef3cf1e4d8ef450f240b", | ||||
|         "type": "github" | ||||
|       }, | ||||
|       "original": { | ||||
|         "owner": "oxalica", | ||||
|         "repo": "rust-overlay", | ||||
|         "type": "github" | ||||
|       } | ||||
|     }, | ||||
|     "systems": { | ||||
|       "locked": { | ||||
|         "lastModified": 1681028828, | ||||
|         "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", | ||||
|         "owner": "nix-systems", | ||||
|         "repo": "default", | ||||
|         "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", | ||||
|         "type": "github" | ||||
|       }, | ||||
|       "original": { | ||||
|         "owner": "nix-systems", | ||||
|         "repo": "default", | ||||
|         "type": "github" | ||||
|       } | ||||
|     }, | ||||
|     "systems_2": { | ||||
|       "locked": { | ||||
|         "lastModified": 1681028828, | ||||
|         "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", | ||||
|         "owner": "nix-systems", | ||||
|         "repo": "default", | ||||
|         "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", | ||||
|         "type": "github" | ||||
|       }, | ||||
|       "original": { | ||||
|         "owner": "nix-systems", | ||||
|         "repo": "default", | ||||
|         "type": "github" | ||||
|       } | ||||
|     } | ||||
|   }, | ||||
|   "root": "root", | ||||
|   "version": 7 | ||||
| } | ||||
							
								
								
									
										60
									
								
								flake.nix
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										60
									
								
								flake.nix
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,60 @@ | ||||
| { | ||||
|   description = "Developpment shell for Plume including nightly Rust compiler"; | ||||
| 
 | ||||
|   inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; | ||||
|   inputs.rust-overlay = { | ||||
|     url = "github:oxalica/rust-overlay"; | ||||
|     inputs.nixpkgs.follows = "nixpkgs"; | ||||
|   }; | ||||
|   inputs.flake-utils.url = "github:numtide/flake-utils"; | ||||
| 
 | ||||
|   outputs = { self, nixpkgs, flake-utils, rust-overlay, ... }: | ||||
|     flake-utils.lib.eachDefaultSystem (system: | ||||
|     let | ||||
|       overlays = [ (import rust-overlay) ]; | ||||
|       pkgs = import nixpkgs { inherit system overlays; }; | ||||
|       inputs = with pkgs; [ | ||||
|             (rust-bin.nightly.latest.default.override { | ||||
|               targets = [ "wasm32-unknown-unknown" ]; | ||||
|             }) | ||||
|             wasm-pack | ||||
|             openssl | ||||
|             pkg-config | ||||
|             gettext | ||||
|             postgresql | ||||
|             sqlite | ||||
|           ]; | ||||
|       in { | ||||
|         packages.default = pkgs.rustPlatform.buildRustPackage { | ||||
|           pname = "plume"; | ||||
|           version = "0.7.3-dev"; | ||||
| 
 | ||||
|           src = ./.; | ||||
| 
 | ||||
|           cargoLock = { | ||||
|             lockFile = ./Cargo.lock; | ||||
|             outputHashes = { | ||||
|               "pulldown-cmark-0.8.0" = "sha256-lpfoRDuY3zJ3QmUqJ5k9OL0MEdGDpwmpJ+u5BCj2kIA="; | ||||
|               "rocket_csrf-0.1.2" = "sha256-WywZfMiwZqTPfSDcAE7ivTSYSaFX+N9fjnRsLSLb9wE="; | ||||
|             }; | ||||
|           }; | ||||
|           buildNoDefaultFeatures = true; | ||||
|           buildFeatures = ["postgresql" "s3"]; | ||||
| 
 | ||||
|           nativeBuildInputs = inputs; | ||||
| 
 | ||||
|           buildPhase = '' | ||||
| 			wasm-pack build --target web --release plume-front | ||||
| 			cargo build --no-default-features --features postgresql,s3 --path . | ||||
| 			cargo build --no-default-features --features postgresql,s3 --path plume-cli | ||||
|           ''; | ||||
|           installPhase = '' | ||||
| 			cargo install --no-default-features --features postgresql,s3 --path . --target-dir $out | ||||
| 			cargo install --no-default-features --features postgresql,s3 --path plume-cli --target-dir $out | ||||
| 		  ''; | ||||
|         }; | ||||
|         devShells.default = pkgs.mkShell { | ||||
|           packages = inputs; | ||||
|         }; | ||||
|       }); | ||||
| } | ||||
| @ -24,3 +24,4 @@ path = "../plume-models" | ||||
| postgres = ["plume-models/postgres", "diesel/postgres"] | ||||
| sqlite = ["plume-models/sqlite", "diesel/sqlite"] | ||||
| search-lindera = ["plume-models/search-lindera"] | ||||
| s3 = ["plume-models/s3"] | ||||
|  | ||||
| @ -18,6 +18,7 @@ rocket_i18n = "0.4.1" | ||||
| reqwest = "0.11.11" | ||||
| scheduled-thread-pool = "0.2.6" | ||||
| serde = "1.0.137" | ||||
| rust-s3 = { version = "0.33.0", optional = true, features = ["blocking"] } | ||||
| serde_derive = "1.0" | ||||
| serde_json = "1.0.81" | ||||
| tantivy = "0.13.3" | ||||
| @ -61,3 +62,4 @@ diesel_migrations = "1.3.0" | ||||
| postgres = ["diesel/postgres", "plume-macro/postgres" ] | ||||
| sqlite = ["diesel/sqlite", "plume-macro/sqlite" ] | ||||
| search-lindera = ["lindera-tantivy"] | ||||
| s3 = ["rust-s3"] | ||||
|  | ||||
| @ -6,6 +6,9 @@ use rocket::Config as RocketConfig; | ||||
| use std::collections::HashSet; | ||||
| use std::env::{self, var}; | ||||
| 
 | ||||
| #[cfg(feature = "s3")] | ||||
| use s3::{Bucket, Region, creds::Credentials}; | ||||
| 
 | ||||
| #[cfg(not(test))] | ||||
| const DB_NAME: &str = "plume"; | ||||
| #[cfg(test)] | ||||
| @ -27,13 +30,23 @@ pub struct Config { | ||||
|     pub mail: Option<MailConfig>, | ||||
|     pub ldap: Option<LdapConfig>, | ||||
|     pub proxy: Option<ProxyConfig>, | ||||
|     pub s3: Option<S3Config>, | ||||
| } | ||||
| 
 | ||||
| impl Config { | ||||
|     pub fn proxy(&self) -> Option<&reqwest::Proxy> { | ||||
|         self.proxy.as_ref().map(|p| &p.proxy) | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| fn string_to_bool(val: &str, name: &str) -> bool { | ||||
|     match val { | ||||
|         "1" | "true" | "TRUE" => true, | ||||
|         "0" | "false" | "FALSE" => false, | ||||
|         _ => panic!("Invalid configuration: {} is not boolean", name), | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| #[derive(Debug, Clone)] | ||||
| pub enum InvalidRocketConfig { | ||||
|     Env, | ||||
| @ -288,11 +301,7 @@ fn get_ldap_config() -> Option<LdapConfig> { | ||||
|     match (addr, base_dn) { | ||||
|         (Some(addr), Some(base_dn)) => { | ||||
|             let tls = var("LDAP_TLS").unwrap_or_else(|_| "false".to_owned()); | ||||
|             let tls = match tls.as_ref() { | ||||
|                 "1" | "true" | "TRUE" => true, | ||||
|                 "0" | "false" | "FALSE" => false, | ||||
|                 _ => panic!("Invalid LDAP configuration : tls"), | ||||
|             }; | ||||
|             let tls = string_to_bool(&tls, "LDAP_TLS"); | ||||
|             let user_name_attr = var("LDAP_USER_NAME_ATTR").unwrap_or_else(|_| "cn".to_owned()); | ||||
|             let mail_attr = var("LDAP_USER_MAIL_ATTR").unwrap_or_else(|_| "mail".to_owned()); | ||||
|             Some(LdapConfig { | ||||
| @ -349,6 +358,104 @@ fn get_proxy_config() -> Option<ProxyConfig> { | ||||
|     }) | ||||
| } | ||||
| 
 | ||||
| pub struct S3Config { | ||||
|     pub bucket: String, | ||||
|     pub access_key_id: String, | ||||
|     pub access_key_secret: String, | ||||
| 
 | ||||
|     // region? If not set, default to us-east-1
 | ||||
|     pub region: String, | ||||
|     // hostname for s3. If not set, default to $region.amazonaws.com
 | ||||
|     pub hostname: String, | ||||
|     // may be useful when using self hosted s3. Won't work with recent AWS buckets
 | ||||
|     pub path_style: bool, | ||||
|     // http or https
 | ||||
|     pub protocol: String, | ||||
| 
 | ||||
|     // download directly from s3 to user, wihout going through Plume. Require public read on bucket
 | ||||
|     pub direct_download: bool, | ||||
|     // use this hostname for downloads, can be used with caching proxy in front of s3 (expected to
 | ||||
|     // be reachable through https)
 | ||||
|     pub alias: Option<String>, | ||||
| } | ||||
| 
 | ||||
| impl S3Config { | ||||
|     #[cfg(feature = "s3")] | ||||
|     pub fn get_bucket(&self) -> Bucket { | ||||
|         let region = Region::Custom { | ||||
|             region: self.region.clone(), | ||||
|             endpoint: format!("{}://{}", self.protocol, self.hostname), | ||||
|         }; | ||||
|         let credentials = Credentials { | ||||
|             access_key: Some(self.access_key_id.clone()), | ||||
|             secret_key: Some(self.access_key_secret.clone()), | ||||
|             security_token: None, | ||||
|             session_token: None, | ||||
|             expiration: None, | ||||
|         }; | ||||
| 
 | ||||
|         let bucket = Bucket::new(&self.bucket, region, credentials).unwrap(); | ||||
|         if self.path_style { | ||||
|             bucket.with_path_style() | ||||
|         } else { | ||||
|             bucket | ||||
|         } | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| fn get_s3_config() -> Option<S3Config> { | ||||
|     let bucket = var("S3_BUCKET").ok(); | ||||
|     let access_key_id = var("AWS_ACCESS_KEY_ID").ok(); | ||||
|     let access_key_secret = var("AWS_SECRET_ACCESS_KEY").ok(); | ||||
|     if bucket.is_none() && access_key_id.is_none() && access_key_secret.is_none() { | ||||
|         return None; | ||||
|     } | ||||
| 
 | ||||
|     #[cfg(not(feature = "s3"))] | ||||
|     panic!("S3 support is not enabled in this build"); | ||||
| 
 | ||||
|     #[cfg(feature = "s3")] | ||||
|     { | ||||
|         if bucket.is_none() || access_key_id.is_none() || access_key_secret.is_none() { | ||||
|             panic!("Invalid S3 configuration: some required values are set, but not others"); | ||||
|         } | ||||
|         let bucket = bucket.unwrap(); | ||||
|         let access_key_id = access_key_id.unwrap(); | ||||
|         let access_key_secret = access_key_secret.unwrap(); | ||||
| 
 | ||||
|         let region = var("S3_REGION").unwrap_or_else(|_| "us-east-1".to_owned()); | ||||
|         let hostname = var("S3_HOSTNAME").unwrap_or_else(|_| format!("{}.amazonaws.com", region)); | ||||
| 
 | ||||
|         let protocol = var("S3_PROTOCOL").unwrap_or_else(|_| "https".to_owned()); | ||||
|         if protocol != "http" && protocol != "https" { | ||||
|             panic!("Invalid S3 configuration: invalid protocol {}", protocol); | ||||
|         } | ||||
| 
 | ||||
|         let path_style = var("S3_PATH_STYLE").unwrap_or_else(|_| "false".to_owned()); | ||||
|         let path_style = string_to_bool(&path_style, "S3_PATH_STYLE"); | ||||
|         let direct_download = var("S3_DIRECT_DOWNLOAD").unwrap_or_else(|_| "false".to_owned()); | ||||
|         let direct_download = string_to_bool(&direct_download, "S3_DIRECT_DOWNLOAD"); | ||||
| 
 | ||||
|         let alias = var("S3_ALIAS_HOST").ok(); | ||||
| 
 | ||||
|         if direct_download && protocol == "http" && alias.is_none() { | ||||
|             panic!("S3 direct download is disabled because bucket is accessed through plain HTTP. Use HTTPS or set an alias hostname (S3_ALIAS_HOST)."); | ||||
|         } | ||||
| 
 | ||||
|         Some(S3Config { | ||||
|             bucket, | ||||
|             access_key_id, | ||||
|             access_key_secret, | ||||
|             region, | ||||
|             hostname, | ||||
|             protocol, | ||||
|             path_style, | ||||
|             direct_download, | ||||
|             alias, | ||||
|         }) | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| lazy_static! { | ||||
|     pub static ref CONFIG: Config = Config { | ||||
|         base_url: var("BASE_URL").unwrap_or_else(|_| format!( | ||||
| @ -380,5 +487,6 @@ lazy_static! { | ||||
|         mail: get_mail_config(), | ||||
|         ldap: get_ldap_config(), | ||||
|         proxy: get_proxy_config(), | ||||
|         s3: get_s3_config(), | ||||
|     }; | ||||
| } | ||||
|  | ||||
| @ -69,6 +69,8 @@ pub enum Error { | ||||
|     Webfinger, | ||||
|     Expired, | ||||
|     UserAlreadyExists, | ||||
|     #[cfg(feature = "s3")] | ||||
|     S3(s3::error::S3Error), | ||||
| } | ||||
| 
 | ||||
| impl From<bcrypt::BcryptError> for Error { | ||||
| @ -170,6 +172,13 @@ impl From<request::Error> for Error { | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| #[cfg(feature = "s3")] | ||||
| impl From<s3::error::S3Error> for Error { | ||||
|     fn from(err: s3::error::S3Error) -> Error { | ||||
|         Error::S3(err) | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| pub type Result<T> = std::result::Result<T, Error>; | ||||
| 
 | ||||
| /// Adds a function to a model, that returns the first
 | ||||
|  | ||||
| @ -16,6 +16,9 @@ use std::{ | ||||
| use tracing::warn; | ||||
| use url::Url; | ||||
| 
 | ||||
| #[cfg(feature = "s3")] | ||||
| use crate::config::S3Config; | ||||
| 
 | ||||
| const REMOTE_MEDIA_DIRECTORY: &str = "remote"; | ||||
| 
 | ||||
| #[derive(Clone, Identifiable, Queryable, AsChangeset)] | ||||
| @ -105,7 +108,7 @@ impl Media { | ||||
|             .file_path | ||||
|             .rsplit_once('.') | ||||
|             .map(|x| x.1) | ||||
|             .expect("Media::category: extension error") | ||||
|             .unwrap_or("") | ||||
|             .to_lowercase() | ||||
|         { | ||||
|             "png" | "jpg" | "jpeg" | "gif" | "svg" => MediaCategory::Image, | ||||
| @ -151,26 +154,99 @@ impl Media { | ||||
|         }) | ||||
|     } | ||||
| 
 | ||||
|     /// Returns full file path for medias stored in the local media directory.
 | ||||
|     pub fn local_path(&self) -> Option<PathBuf> { | ||||
|         if self.file_path.is_empty() { | ||||
|             return None; | ||||
|         } | ||||
| 
 | ||||
|         if CONFIG.s3.is_some() { | ||||
|             #[cfg(feature="s3")] | ||||
|             unreachable!("Called Media::local_path() but media are stored on S3"); | ||||
|             #[cfg(not(feature="s3"))] | ||||
|             unreachable!(); | ||||
|         } | ||||
| 
 | ||||
|         let relative_path = self | ||||
|             .file_path | ||||
|             .trim_start_matches(&CONFIG.media_directory) | ||||
|             .trim_start_matches(path::MAIN_SEPARATOR) | ||||
|             .trim_start_matches("static/media/"); | ||||
| 
 | ||||
|         Some(Path::new(&CONFIG.media_directory).join(relative_path)) | ||||
|     } | ||||
| 
 | ||||
|     /// Returns the relative URL to access this file, which is also the key at which
 | ||||
|     /// it is stored in the S3 bucket if we are using S3 storage.
 | ||||
|     /// Does not start with a '/', it is of the form "static/media/<...>"
 | ||||
|     pub fn relative_url(&self) -> Option<String> { | ||||
|         if self.file_path.is_empty() { | ||||
|             return None; | ||||
|         } | ||||
| 
 | ||||
|         let relative_path = self | ||||
|             .file_path | ||||
|             .trim_start_matches(&CONFIG.media_directory) | ||||
|             .replace(path::MAIN_SEPARATOR, "/"); | ||||
| 
 | ||||
|         let relative_path = relative_path | ||||
|             .trim_start_matches('/') | ||||
|             .trim_start_matches("static/media/"); | ||||
| 
 | ||||
|         Some(format!("static/media/{}", relative_path)) | ||||
|     } | ||||
| 
 | ||||
|     /// Returns a public URL through which this media file can be accessed
 | ||||
|     pub fn url(&self) -> Result<String> { | ||||
|         if self.is_remote { | ||||
|             Ok(self.remote_url.clone().unwrap_or_default()) | ||||
|         } else { | ||||
|             let file_path = self.file_path.replace(path::MAIN_SEPARATOR, "/").replacen( | ||||
|                 &CONFIG.media_directory, | ||||
|                 "static/media", | ||||
|                 1, | ||||
|             ); // "static/media" from plume::routs::plume_media_files()
 | ||||
|             let relative_url = self.relative_url().unwrap_or_default(); | ||||
| 
 | ||||
|             #[cfg(feature="s3")] | ||||
|             if CONFIG.s3.as_ref().map(|x| x.direct_download).unwrap_or(false) { | ||||
|                 let s3_url = match CONFIG.s3.as_ref().unwrap() { | ||||
|                     S3Config { alias: Some(alias), .. } => { | ||||
|                         format!("https://{}/{}", alias, relative_url) | ||||
|                     } | ||||
|                     S3Config { path_style: true, hostname, bucket, .. } => { | ||||
|                         format!("https://{}/{}/{}", | ||||
|                             hostname, | ||||
|                             bucket, | ||||
|                             relative_url | ||||
|                         ) | ||||
|                     } | ||||
|                     S3Config { path_style: false, hostname, bucket, .. } => { | ||||
|                         format!("https://{}.{}/{}", | ||||
|                             bucket, | ||||
|                             hostname, | ||||
|                             relative_url | ||||
|                         ) | ||||
|                     } | ||||
|                 }; | ||||
|                 return Ok(s3_url); | ||||
|             } | ||||
| 
 | ||||
|             Ok(ap_url(&format!( | ||||
|                 "{}/{}", | ||||
|                 Instance::get_local()?.public_domain, | ||||
|                 &file_path | ||||
|                 relative_url | ||||
|             ))) | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     pub fn delete(&self, conn: &Connection) -> Result<()> { | ||||
|         if !self.is_remote { | ||||
|             fs::remove_file(self.file_path.as_str())?; | ||||
|             if CONFIG.s3.is_some() { | ||||
|                 #[cfg(not(feature="s3"))] | ||||
|                 unreachable!(); | ||||
| 
 | ||||
|                 #[cfg(feature = "s3")] | ||||
|                 CONFIG.s3.as_ref().unwrap().get_bucket() | ||||
|                     .delete_object_blocking(&self.relative_url().ok_or(Error::NotFound)?)?; | ||||
|             } else { | ||||
|                 fs::remove_file(self.local_path().ok_or(Error::NotFound)?)?; | ||||
|             } | ||||
|         } | ||||
|         diesel::delete(self) | ||||
|             .execute(conn) | ||||
| @ -211,22 +287,60 @@ impl Media { | ||||
|             .url() | ||||
|             .and_then(|url| url.to_as_uri()) | ||||
|             .ok_or(Error::MissingApProperty)?; | ||||
|         let path = determine_mirror_file_path(&remote_url); | ||||
|         let parent = path.parent().ok_or(Error::InvalidValue)?; | ||||
|         if !parent.is_dir() { | ||||
|             DirBuilder::new().recursive(true).create(parent)?; | ||||
|         } | ||||
| 
 | ||||
|         let mut dest = fs::File::create(path.clone())?; | ||||
|         // TODO: conditional GET
 | ||||
|         request::get( | ||||
|             remote_url.as_str(), | ||||
|             User::get_sender(), | ||||
|             CONFIG.proxy().cloned(), | ||||
|         )? | ||||
|         .copy_to(&mut dest)?; | ||||
|         let file_path = if CONFIG.s3.is_some() { | ||||
|             #[cfg(not(feature="s3"))] | ||||
|             unreachable!(); | ||||
| 
 | ||||
|         Media::find_by_file_path(conn, path.to_str().ok_or(Error::InvalidValue)?) | ||||
|             #[cfg(feature = "s3")] | ||||
|             { | ||||
|                 use rocket::http::ContentType; | ||||
| 
 | ||||
|                 let dest = determine_mirror_s3_path(&remote_url); | ||||
| 
 | ||||
|                 let media = request::get( | ||||
|                     remote_url.as_str(), | ||||
|                     User::get_sender(), | ||||
|                     CONFIG.proxy().cloned(), | ||||
|                 )?; | ||||
| 
 | ||||
|                 let content_type = media | ||||
|                     .headers() | ||||
|                     .get(reqwest::header::CONTENT_TYPE) | ||||
|                     .and_then(|x| x.to_str().ok()) | ||||
|                     .and_then(ContentType::parse_flexible) | ||||
|                     .unwrap_or(ContentType::Binary); | ||||
| 
 | ||||
|                 let bytes = media.bytes()?; | ||||
| 
 | ||||
|                 let bucket = CONFIG.s3.as_ref().unwrap().get_bucket(); | ||||
|                 bucket.put_object_with_content_type_blocking( | ||||
|                     &dest, | ||||
|                     &bytes, | ||||
|                     &content_type.to_string() | ||||
|                 )?; | ||||
| 
 | ||||
|                 dest | ||||
|             } | ||||
|         } else { | ||||
|             let path = determine_mirror_file_path(&remote_url); | ||||
|             let parent = path.parent().ok_or(Error::InvalidValue)?; | ||||
|             if !parent.is_dir() { | ||||
|                 DirBuilder::new().recursive(true).create(parent)?; | ||||
|             } | ||||
| 
 | ||||
|             let mut dest = fs::File::create(path.clone())?; | ||||
|             // TODO: conditional GET
 | ||||
|             request::get( | ||||
|                 remote_url.as_str(), | ||||
|                 User::get_sender(), | ||||
|                 CONFIG.proxy().cloned(), | ||||
|             )? | ||||
|             .copy_to(&mut dest)?; | ||||
|             path.to_str().ok_or(Error::InvalidValue)?.to_string() | ||||
|         }; | ||||
| 
 | ||||
|         Media::find_by_file_path(conn, &file_path) | ||||
|             .and_then(|mut media| { | ||||
|                 let mut updated = false; | ||||
| 
 | ||||
| @ -267,7 +381,7 @@ impl Media { | ||||
|                 Media::insert( | ||||
|                     conn, | ||||
|                     NewMedia { | ||||
|                         file_path: path.to_str().ok_or(Error::InvalidValue)?.to_string(), | ||||
|                         file_path, | ||||
|                         alt_text: image | ||||
|                             .content() | ||||
|                             .and_then(|content| content.to_as_string()) | ||||
| @ -307,12 +421,10 @@ impl Media { | ||||
| } | ||||
| 
 | ||||
| fn determine_mirror_file_path(url: &str) -> PathBuf { | ||||
|     let mut file_path = Path::new(&super::CONFIG.media_directory).join(REMOTE_MEDIA_DIRECTORY); | ||||
|     Url::parse(url) | ||||
|         .map(|url| { | ||||
|             if !url.has_host() { | ||||
|                 return; | ||||
|             } | ||||
|     let mut file_path = Path::new(&CONFIG.media_directory).join(REMOTE_MEDIA_DIRECTORY); | ||||
| 
 | ||||
|     match Url::parse(url) { | ||||
|         Ok(url) if url.has_host() => { | ||||
|             file_path.push(url.host_str().unwrap()); | ||||
|             for segment in url.path_segments().expect("FIXME") { | ||||
|                 file_path.push(segment); | ||||
| @ -320,19 +432,54 @@ fn determine_mirror_file_path(url: &str) -> PathBuf { | ||||
|             // TODO: handle query
 | ||||
|             // HINT: Use characters which must be percent-encoded in path as separator between path and query
 | ||||
|             // HINT: handle extension
 | ||||
|         }) | ||||
|         .unwrap_or_else(|err| { | ||||
|             warn!("Failed to parse url: {} {}", &url, err); | ||||
|         } | ||||
|         other => { | ||||
|             if let Err(err) = other { | ||||
|                 warn!("Failed to parse url: {} {}", &url, err); | ||||
|             } else { | ||||
|                 warn!("Error without a host: {}", &url); | ||||
|             } | ||||
|             let ext = url | ||||
|                 .rsplit('.') | ||||
|                 .next() | ||||
|                 .map(ToOwned::to_owned) | ||||
|                 .unwrap_or_else(|| String::from("png")); | ||||
|             file_path.push(format!("{}.{}", GUID::rand(), ext)); | ||||
|         }); | ||||
|         } | ||||
|     } | ||||
|     file_path | ||||
| } | ||||
| 
 | ||||
| #[cfg(feature="s3")] | ||||
| fn determine_mirror_s3_path(url: &str) -> String { | ||||
|     match Url::parse(url) { | ||||
|         Ok(url) if url.has_host() => { | ||||
|             format!("static/media/{}/{}/{}", | ||||
|                 REMOTE_MEDIA_DIRECTORY, | ||||
|                 url.host_str().unwrap(), | ||||
|                 url.path().trim_start_matches('/'), | ||||
|             ) | ||||
|         } | ||||
|         other => { | ||||
|             if let Err(err) = other { | ||||
|                 warn!("Failed to parse url: {} {}", &url, err); | ||||
|             } else { | ||||
|                 warn!("Error without a host: {}", &url); | ||||
|             } | ||||
|             let ext = url | ||||
|                 .rsplit('.') | ||||
|                 .next() | ||||
|                 .map(ToOwned::to_owned) | ||||
|                 .unwrap_or_else(|| String::from("png")); | ||||
|             format!("static/media/{}/{}.{}", | ||||
|                 REMOTE_MEDIA_DIRECTORY, | ||||
|                 GUID::rand(), | ||||
|                 ext, | ||||
|             ) | ||||
|         } | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| #[cfg(test)] | ||||
| pub(crate) mod tests { | ||||
|     use super::*; | ||||
|  | ||||
| @ -2,7 +2,7 @@ use crate::routes::{errors::ErrorPage, Page}; | ||||
| use crate::template_utils::{IntoContext, Ructe}; | ||||
| use guid_create::GUID; | ||||
| use multipart::server::{ | ||||
|     save::{SaveResult, SavedData}, | ||||
|     save::{SaveResult, SavedField, SavedData}, | ||||
|     Multipart, | ||||
| }; | ||||
| use plume_models::{db_conn::DbConn, medias::*, users::User, Error, PlumeRocket, CONFIG}; | ||||
| @ -55,41 +55,16 @@ pub fn upload( | ||||
|     if let SaveResult::Full(entries) = Multipart::with_body(data.open(), boundary).save().temp() { | ||||
|         let fields = entries.fields; | ||||
| 
 | ||||
|         let filename = fields | ||||
|         let file = fields | ||||
|             .get("file") | ||||
|             .and_then(|v| v.iter().next()) | ||||
|             .ok_or(status::BadRequest(Some("No file uploaded")))? | ||||
|             .headers | ||||
|             .filename | ||||
|             .clone(); | ||||
|         // Remove extension if it contains something else than just letters and numbers
 | ||||
|         let ext = filename | ||||
|             .and_then(|f| { | ||||
|                 f.rsplit('.') | ||||
|                     .next() | ||||
|                     .and_then(|ext| { | ||||
|                         if ext.chars().any(|c| !c.is_alphanumeric()) { | ||||
|                             None | ||||
|                         } else { | ||||
|                             Some(ext.to_lowercase()) | ||||
|                         } | ||||
|                     }) | ||||
|                     .map(|ext| format!(".{}", ext)) | ||||
|             }) | ||||
|             .unwrap_or_default(); | ||||
|         let dest = format!("{}/{}{}", CONFIG.media_directory, GUID::rand(), ext); | ||||
|             .ok_or(status::BadRequest(Some("No file uploaded")))?; | ||||
| 
 | ||||
|         match fields["file"][0].data { | ||||
|             SavedData::Bytes(ref bytes) => fs::write(&dest, bytes) | ||||
|                 .map_err(|_| status::BadRequest(Some("Couldn't save upload")))?, | ||||
|             SavedData::File(ref path, _) => { | ||||
|                 fs::copy(path, &dest) | ||||
|                     .map_err(|_| status::BadRequest(Some("Couldn't copy upload")))?; | ||||
|             } | ||||
|             _ => { | ||||
|                 return Ok(Redirect::to(uri!(new))); | ||||
|             } | ||||
|         } | ||||
|         let file_path = match save_uploaded_file(file) { | ||||
|             Ok(Some(file_path)) => file_path, | ||||
|             Ok(None) => return Ok(Redirect::to(uri!(new))), | ||||
|             Err(_) => return Err(status::BadRequest(Some("Couldn't save uploaded media: {}"))), | ||||
|         }; | ||||
| 
 | ||||
|         let has_cw = !read(&fields["cw"][0].data) | ||||
|             .map(|cw| cw.is_empty()) | ||||
| @ -97,7 +72,7 @@ pub fn upload( | ||||
|         let media = Media::insert( | ||||
|             &conn, | ||||
|             NewMedia { | ||||
|                 file_path: dest, | ||||
|                 file_path, | ||||
|                 alt_text: read(&fields["alt"][0].data)?, | ||||
|                 is_remote: false, | ||||
|                 remote_url: None, | ||||
| @ -117,6 +92,74 @@ pub fn upload( | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| fn save_uploaded_file(file: &SavedField) -> Result<Option<String>, plume_models::Error> { | ||||
|     // Remove extension if it contains something else than just letters and numbers
 | ||||
|     let ext = file | ||||
|         .headers | ||||
|         .filename | ||||
|         .as_ref() | ||||
|         .and_then(|f| { | ||||
|             f.rsplit('.') | ||||
|                 .next() | ||||
|                 .and_then(|ext| { | ||||
|                     if ext.chars().any(|c| !c.is_alphanumeric()) { | ||||
|                         None | ||||
|                     } else { | ||||
|                         Some(ext.to_lowercase()) | ||||
|                     } | ||||
|                 }) | ||||
|         }) | ||||
|         .unwrap_or_default(); | ||||
| 
 | ||||
|     if CONFIG.s3.is_some() { | ||||
|         #[cfg(not(feature="s3"))] | ||||
|         unreachable!(); | ||||
| 
 | ||||
|         #[cfg(feature="s3")] | ||||
|         { | ||||
|             use std::borrow::Cow; | ||||
| 
 | ||||
|             let dest = format!("static/media/{}.{}", GUID::rand(), ext); | ||||
| 
 | ||||
|             let bytes = match file.data { | ||||
|                 SavedData::Bytes(ref bytes) => Cow::from(bytes), | ||||
|                 SavedData::File(ref path, _) => Cow::from(fs::read(path)?), | ||||
|                 _ => { | ||||
|                     return Ok(None); | ||||
|                 } | ||||
|             }; | ||||
| 
 | ||||
|             let bucket = CONFIG.s3.as_ref().unwrap().get_bucket(); | ||||
|             let content_type = match &file.headers.content_type { | ||||
|                 Some(ct) => ct.to_string(), | ||||
|                 None => ContentType::from_extension(&ext) | ||||
|                     .unwrap_or(ContentType::Binary) | ||||
|                     .to_string(), | ||||
|             }; | ||||
| 
 | ||||
|             bucket.put_object_with_content_type_blocking(&dest, &bytes, &content_type)?; | ||||
| 
 | ||||
|             Ok(Some(dest)) | ||||
|         } | ||||
|     } else { | ||||
|         let dest = format!("{}/{}.{}", CONFIG.media_directory, GUID::rand(), ext); | ||||
| 
 | ||||
|         match file.data { | ||||
|             SavedData::Bytes(ref bytes) => { | ||||
|                 fs::write(&dest, bytes)?; | ||||
|             } | ||||
|             SavedData::File(ref path, _) => { | ||||
|                 fs::copy(path, &dest)?; | ||||
|             } | ||||
|             _ => { | ||||
|                 return Ok(None); | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         Ok(Some(dest)) | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| fn read(data: &SavedData) -> Result<String, status::BadRequest<&'static str>> { | ||||
|     if let SavedData::Text(s) = data { | ||||
|         Ok(s.clone()) | ||||
|  | ||||
| @ -21,6 +21,9 @@ use std::{ | ||||
|     path::{Path, PathBuf}, | ||||
| }; | ||||
| 
 | ||||
| #[cfg(feature = "s3")] | ||||
| use rocket::http::ContentType; | ||||
| 
 | ||||
| /// Special return type used for routes that "cannot fail", and instead
 | ||||
| /// `Redirect`, or `Flash<Redirect>`, when we cannot deliver a `Ructe` Response
 | ||||
| #[allow(clippy::large_enum_variant)] | ||||
| @ -204,10 +207,17 @@ pub mod timelines; | ||||
| pub mod user; | ||||
| pub mod well_known; | ||||
| 
 | ||||
| #[derive(Responder)] | ||||
| enum FileKind { | ||||
|     Local(NamedFile), | ||||
|     #[cfg(feature = "s3")] | ||||
|     S3(Vec<u8>, ContentType), | ||||
| } | ||||
| 
 | ||||
| #[derive(Responder)] | ||||
| #[response()] | ||||
| pub struct CachedFile { | ||||
|     inner: NamedFile, | ||||
|     inner: FileKind, | ||||
|     cache_control: CacheControl, | ||||
| } | ||||
| 
 | ||||
| @ -253,19 +263,41 @@ pub fn plume_static_files(file: PathBuf, build_id: &RawStr) -> Option<CachedFile | ||||
| } | ||||
| #[get("/static/media/<file..>")] | ||||
| pub fn plume_media_files(file: PathBuf) -> Option<CachedFile> { | ||||
|     NamedFile::open(Path::new(&CONFIG.media_directory).join(file)) | ||||
|         .ok() | ||||
|         .map(|f| CachedFile { | ||||
|             inner: f, | ||||
|             cache_control: CacheControl(vec![CacheDirective::MaxAge(60 * 60 * 24 * 30)]), | ||||
|         }) | ||||
|     if CONFIG.s3.is_some() { | ||||
|         #[cfg(not(feature="s3"))] | ||||
|         unreachable!(); | ||||
| 
 | ||||
|         #[cfg(feature="s3")] | ||||
|         { | ||||
|             let data = CONFIG.s3.as_ref().unwrap().get_bucket() | ||||
|                 .get_object_blocking(format!("static/media/{}", file.to_string_lossy())).ok()?; | ||||
| 
 | ||||
|             let ct = data.headers().get("content-type") | ||||
|                 .and_then(|x| ContentType::parse_flexible(&x)) | ||||
|                 .or_else(|| file.extension() | ||||
|                     .and_then(|ext| ContentType::from_extension(&ext.to_string_lossy()))) | ||||
|                 .unwrap_or(ContentType::Binary); | ||||
| 
 | ||||
|             Some(CachedFile { | ||||
|                 inner: FileKind::S3(data.to_vec(), ct), | ||||
|                 cache_control: CacheControl(vec![CacheDirective::MaxAge(60 * 60 * 24 * 30)]), | ||||
|             }) | ||||
|         } | ||||
|     } else { | ||||
|         NamedFile::open(Path::new(&CONFIG.media_directory).join(file)) | ||||
|             .ok() | ||||
|             .map(|f| CachedFile { | ||||
|                 inner: FileKind::Local(f), | ||||
|                 cache_control: CacheControl(vec![CacheDirective::MaxAge(60 * 60 * 24 * 30)]), | ||||
|             }) | ||||
|     } | ||||
| } | ||||
| #[get("/static/<file..>", rank = 3)] | ||||
| pub fn static_files(file: PathBuf) -> Option<CachedFile> { | ||||
|     NamedFile::open(Path::new("static/").join(file)) | ||||
|         .ok() | ||||
|         .map(|f| CachedFile { | ||||
|             inner: f, | ||||
|             inner: FileKind::Local(f), | ||||
|             cache_control: CacheControl(vec![CacheDirective::MaxAge(60 * 60 * 24 * 30)]), | ||||
|         }) | ||||
| } | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user