Merge pull request #16 from stevedylandev/feat/posts-enhancements
579c3624
feat: added posts enhancements
10 file(s) · +487 −9
feat: added posts enhancements
| 9 | 9 | checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" |
|
| 10 | 10 | ||
| 11 | 11 | [[package]] |
|
| 12 | + | name = "aes" |
|
| 13 | + | version = "0.8.4" |
|
| 14 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 15 | + | checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" |
|
| 16 | + | dependencies = [ |
|
| 17 | + | "cfg-if", |
|
| 18 | + | "cipher", |
|
| 19 | + | "cpufeatures", |
|
| 20 | + | ] |
|
| 21 | + | ||
| 22 | + | [[package]] |
|
| 12 | 23 | name = "aho-corasick" |
|
| 13 | 24 | version = "1.1.4" |
|
| 14 | 25 | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 120 | 131 | version = "1.4.2" |
|
| 121 | 132 | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 122 | 133 | checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" |
|
| 134 | + | dependencies = [ |
|
| 135 | + | "derive_arbitrary", |
|
| 136 | + | ] |
|
| 123 | 137 | ||
| 124 | 138 | [[package]] |
|
| 125 | 139 | name = "arboard" |
|
| 622 | 636 | checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" |
|
| 623 | 637 | ||
| 624 | 638 | [[package]] |
|
| 639 | + | name = "bzip2" |
|
| 640 | + | version = "0.5.2" |
|
| 641 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 642 | + | checksum = "49ecfb22d906f800d4fe833b6282cf4dc1c298f5057ca0b5445e5c209735ca47" |
|
| 643 | + | dependencies = [ |
|
| 644 | + | "bzip2-sys", |
|
| 645 | + | ] |
|
| 646 | + | ||
| 647 | + | [[package]] |
|
| 648 | + | name = "bzip2-sys" |
|
| 649 | + | version = "0.1.13+1.0.8" |
|
| 650 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 651 | + | checksum = "225bff33b2141874fe80d71e07d6eec4f85c5c216453dd96388240f96e1acc14" |
|
| 652 | + | dependencies = [ |
|
| 653 | + | "cc", |
|
| 654 | + | "pkg-config", |
|
| 655 | + | ] |
|
| 656 | + | ||
| 657 | + | [[package]] |
|
| 625 | 658 | name = "castaway" |
|
| 626 | 659 | version = "0.2.4" |
|
| 627 | 660 | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 699 | 732 | ] |
|
| 700 | 733 | ||
| 701 | 734 | [[package]] |
|
| 735 | + | name = "cipher" |
|
| 736 | + | version = "0.4.4" |
|
| 737 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 738 | + | checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" |
|
| 739 | + | dependencies = [ |
|
| 740 | + | "crypto-common", |
|
| 741 | + | "inout", |
|
| 742 | + | ] |
|
| 743 | + | ||
| 744 | + | [[package]] |
|
| 702 | 745 | name = "clap" |
|
| 703 | 746 | version = "4.6.0" |
|
| 704 | 747 | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 793 | 836 | ] |
|
| 794 | 837 | ||
| 795 | 838 | [[package]] |
|
| 839 | + | name = "constant_time_eq" |
|
| 840 | + | version = "0.3.1" |
|
| 841 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 842 | + | checksum = "7c74b8349d32d297c9134b8c88677813a227df8f779daa29bfc29c183fe3dca6" |
|
| 843 | + | ||
| 844 | + | [[package]] |
|
| 796 | 845 | name = "convert_case" |
|
| 797 | 846 | version = "0.10.0" |
|
| 798 | 847 | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 846 | 895 | ] |
|
| 847 | 896 | ||
| 848 | 897 | [[package]] |
|
| 898 | + | name = "crc" |
|
| 899 | + | version = "3.4.0" |
|
| 900 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 901 | + | checksum = "5eb8a2a1cd12ab0d987a5d5e825195d372001a4094a0376319d5a0ad71c1ba0d" |
|
| 902 | + | dependencies = [ |
|
| 903 | + | "crc-catalog", |
|
| 904 | + | ] |
|
| 905 | + | ||
| 906 | + | [[package]] |
|
| 907 | + | name = "crc-catalog" |
|
| 908 | + | version = "2.4.0" |
|
| 909 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 910 | + | checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" |
|
| 911 | + | ||
| 912 | + | [[package]] |
|
| 849 | 913 | name = "crc32fast" |
|
| 850 | 914 | version = "1.5.0" |
|
| 851 | 915 | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 990 | 1054 | ] |
|
| 991 | 1055 | ||
| 992 | 1056 | [[package]] |
|
| 1057 | + | name = "deflate64" |
|
| 1058 | + | version = "0.1.12" |
|
| 1059 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 1060 | + | checksum = "ac6b926516df9c60bfa16e107b21086399f8285a44ca9711344b9e553c5146e2" |
|
| 1061 | + | ||
| 1062 | + | [[package]] |
|
| 993 | 1063 | name = "deltae" |
|
| 994 | 1064 | version = "0.3.2" |
|
| 995 | 1065 | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 1002 | 1072 | checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" |
|
| 1003 | 1073 | dependencies = [ |
|
| 1004 | 1074 | "powerfmt", |
|
| 1075 | + | ] |
|
| 1076 | + | ||
| 1077 | + | [[package]] |
|
| 1078 | + | name = "derive_arbitrary" |
|
| 1079 | + | version = "1.4.2" |
|
| 1080 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 1081 | + | checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a" |
|
| 1082 | + | dependencies = [ |
|
| 1083 | + | "proc-macro2", |
|
| 1084 | + | "quote", |
|
| 1085 | + | "syn 2.0.117", |
|
| 1005 | 1086 | ] |
|
| 1006 | 1087 | ||
| 1007 | 1088 | [[package]] |
|
| 1045 | 1126 | dependencies = [ |
|
| 1046 | 1127 | "block-buffer", |
|
| 1047 | 1128 | "crypto-common", |
|
| 1129 | + | "subtle", |
|
| 1048 | 1130 | ] |
|
| 1049 | 1131 | ||
| 1050 | 1132 | [[package]] |
|
| 1593 | 1675 | checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" |
|
| 1594 | 1676 | ||
| 1595 | 1677 | [[package]] |
|
| 1678 | + | name = "hmac" |
|
| 1679 | + | version = "0.12.1" |
|
| 1680 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 1681 | + | checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" |
|
| 1682 | + | dependencies = [ |
|
| 1683 | + | "digest", |
|
| 1684 | + | ] |
|
| 1685 | + | ||
| 1686 | + | [[package]] |
|
| 1596 | 1687 | name = "html5ever" |
|
| 1597 | 1688 | version = "0.29.1" |
|
| 1598 | 1689 | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 1956 | 2047 | ] |
|
| 1957 | 2048 | ||
| 1958 | 2049 | [[package]] |
|
| 2050 | + | name = "inout" |
|
| 2051 | + | version = "0.1.4" |
|
| 2052 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 2053 | + | checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" |
|
| 2054 | + | dependencies = [ |
|
| 2055 | + | "generic-array", |
|
| 2056 | + | ] |
|
| 2057 | + | ||
| 2058 | + | [[package]] |
|
| 1959 | 2059 | name = "instability" |
|
| 1960 | 2060 | version = "0.3.12" |
|
| 1961 | 2061 | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 2264 | 2364 | checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" |
|
| 2265 | 2365 | ||
| 2266 | 2366 | [[package]] |
|
| 2367 | + | name = "lzma-rs" |
|
| 2368 | + | version = "0.3.0" |
|
| 2369 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 2370 | + | checksum = "297e814c836ae64db86b36cf2a557ba54368d03f6afcd7d947c266692f71115e" |
|
| 2371 | + | dependencies = [ |
|
| 2372 | + | "byteorder", |
|
| 2373 | + | "crc", |
|
| 2374 | + | ] |
|
| 2375 | + | ||
| 2376 | + | [[package]] |
|
| 2377 | + | name = "lzma-sys" |
|
| 2378 | + | version = "0.1.20" |
|
| 2379 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 2380 | + | checksum = "5fda04ab3764e6cde78b9974eec4f779acaba7c4e84b36eca3cf77c581b85d27" |
|
| 2381 | + | dependencies = [ |
|
| 2382 | + | "cc", |
|
| 2383 | + | "libc", |
|
| 2384 | + | "pkg-config", |
|
| 2385 | + | ] |
|
| 2386 | + | ||
| 2387 | + | [[package]] |
|
| 2267 | 2388 | name = "mac" |
|
| 2268 | 2389 | version = "0.1.1" |
|
| 2269 | 2390 | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 2827 | 2948 | checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3" |
|
| 2828 | 2949 | ||
| 2829 | 2950 | [[package]] |
|
| 2951 | + | name = "pbkdf2" |
|
| 2952 | + | version = "0.12.2" |
|
| 2953 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 2954 | + | checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2" |
|
| 2955 | + | dependencies = [ |
|
| 2956 | + | "digest", |
|
| 2957 | + | "hmac", |
|
| 2958 | + | ] |
|
| 2959 | + | ||
| 2960 | + | [[package]] |
|
| 2830 | 2961 | name = "percent-encoding" |
|
| 2831 | 2962 | version = "2.3.2" |
|
| 2832 | 2963 | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 2991 | 3122 | "tokio", |
|
| 2992 | 3123 | "tracing", |
|
| 2993 | 3124 | "tracing-subscriber", |
|
| 3125 | + | "zip", |
|
| 2994 | 3126 | ] |
|
| 2995 | 3127 | ||
| 2996 | 3128 | [[package]] |
|
| 3924 | 4056 | ] |
|
| 3925 | 4057 | ||
| 3926 | 4058 | [[package]] |
|
| 4059 | + | name = "sha1" |
|
| 4060 | + | version = "0.10.6" |
|
| 4061 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 4062 | + | checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" |
|
| 4063 | + | dependencies = [ |
|
| 4064 | + | "cfg-if", |
|
| 4065 | + | "cpufeatures", |
|
| 4066 | + | "digest", |
|
| 4067 | + | ] |
|
| 4068 | + | ||
| 4069 | + | [[package]] |
|
| 3927 | 4070 | name = "sha2" |
|
| 3928 | 4071 | version = "0.10.9" |
|
| 3929 | 4072 | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 5525 | 5668 | checksum = "ea6fc2961e4ef194dcbfe56bb845534d0dc8098940c7e5c012a258bfec6701bd" |
|
| 5526 | 5669 | ||
| 5527 | 5670 | [[package]] |
|
| 5671 | + | name = "xz2" |
|
| 5672 | + | version = "0.1.7" |
|
| 5673 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 5674 | + | checksum = "388c44dc09d76f1536602ead6d325eb532f5c122f17782bd57fb47baeeb767e2" |
|
| 5675 | + | dependencies = [ |
|
| 5676 | + | "lzma-sys", |
|
| 5677 | + | ] |
|
| 5678 | + | ||
| 5679 | + | [[package]] |
|
| 5528 | 5680 | name = "y4m" |
|
| 5529 | 5681 | version = "0.8.0" |
|
| 5530 | 5682 | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 5608 | 5760 | version = "1.8.2" |
|
| 5609 | 5761 | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 5610 | 5762 | checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" |
|
| 5763 | + | dependencies = [ |
|
| 5764 | + | "zeroize_derive", |
|
| 5765 | + | ] |
|
| 5766 | + | ||
| 5767 | + | [[package]] |
|
| 5768 | + | name = "zeroize_derive" |
|
| 5769 | + | version = "1.4.3" |
|
| 5770 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 5771 | + | checksum = "85a5b4158499876c763cb03bc4e49185d3cccbabb15b33c627f7884f43db852e" |
|
| 5772 | + | dependencies = [ |
|
| 5773 | + | "proc-macro2", |
|
| 5774 | + | "quote", |
|
| 5775 | + | "syn 2.0.117", |
|
| 5776 | + | ] |
|
| 5611 | 5777 | ||
| 5612 | 5778 | [[package]] |
|
| 5613 | 5779 | name = "zerotrie" |
|
| 5643 | 5809 | ] |
|
| 5644 | 5810 | ||
| 5645 | 5811 | [[package]] |
|
| 5812 | + | name = "zip" |
|
| 5813 | + | version = "2.4.2" |
|
| 5814 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 5815 | + | checksum = "fabe6324e908f85a1c52063ce7aa26b68dcb7eb6dbc83a2d148403c9bc3eba50" |
|
| 5816 | + | dependencies = [ |
|
| 5817 | + | "aes", |
|
| 5818 | + | "arbitrary", |
|
| 5819 | + | "bzip2", |
|
| 5820 | + | "constant_time_eq", |
|
| 5821 | + | "crc32fast", |
|
| 5822 | + | "crossbeam-utils", |
|
| 5823 | + | "deflate64", |
|
| 5824 | + | "displaydoc", |
|
| 5825 | + | "flate2", |
|
| 5826 | + | "getrandom 0.3.4", |
|
| 5827 | + | "hmac", |
|
| 5828 | + | "indexmap", |
|
| 5829 | + | "lzma-rs", |
|
| 5830 | + | "memchr", |
|
| 5831 | + | "pbkdf2", |
|
| 5832 | + | "sha1", |
|
| 5833 | + | "thiserror 2.0.18", |
|
| 5834 | + | "time", |
|
| 5835 | + | "xz2", |
|
| 5836 | + | "zeroize", |
|
| 5837 | + | "zopfli", |
|
| 5838 | + | "zstd", |
|
| 5839 | + | ] |
|
| 5840 | + | ||
| 5841 | + | [[package]] |
|
| 5646 | 5842 | name = "zmij" |
|
| 5647 | 5843 | version = "1.0.21" |
|
| 5648 | 5844 | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 5649 | 5845 | checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" |
|
| 5846 | + | ||
| 5847 | + | [[package]] |
|
| 5848 | + | name = "zopfli" |
|
| 5849 | + | version = "0.8.3" |
|
| 5850 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 5851 | + | checksum = "f05cd8797d63865425ff89b5c4a48804f35ba0ce8d125800027ad6017d2b5249" |
|
| 5852 | + | dependencies = [ |
|
| 5853 | + | "bumpalo", |
|
| 5854 | + | "crc32fast", |
|
| 5855 | + | "log", |
|
| 5856 | + | "simd-adler32", |
|
| 5857 | + | ] |
|
| 5858 | + | ||
| 5859 | + | [[package]] |
|
| 5860 | + | name = "zstd" |
|
| 5861 | + | version = "0.13.3" |
|
| 5862 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 5863 | + | checksum = "e91ee311a569c327171651566e07972200e76fcfe2242a4fa446149a3881c08a" |
|
| 5864 | + | dependencies = [ |
|
| 5865 | + | "zstd-safe", |
|
| 5866 | + | ] |
|
| 5867 | + | ||
| 5868 | + | [[package]] |
|
| 5869 | + | name = "zstd-safe" |
|
| 5870 | + | version = "7.2.4" |
|
| 5871 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 5872 | + | checksum = "8f49c4d5f0abb602a93fb8736af2a4f4dd9512e36f7f570d66e65ff867ed3b9d" |
|
| 5873 | + | dependencies = [ |
|
| 5874 | + | "zstd-sys", |
|
| 5875 | + | ] |
|
| 5876 | + | ||
| 5877 | + | [[package]] |
|
| 5878 | + | name = "zstd-sys" |
|
| 5879 | + | version = "2.0.16+zstd.1.5.7" |
|
| 5880 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 5881 | + | checksum = "91e19ebc2adc8f83e43039e79776e3fda8ca919132d68a1fed6a5faca2683748" |
|
| 5882 | + | dependencies = [ |
|
| 5883 | + | "cc", |
|
| 5884 | + | "pkg-config", |
|
| 5885 | + | ] |
|
| 5650 | 5886 | ||
| 5651 | 5887 | [[package]] |
|
| 5652 | 5888 | name = "zune-core" |
|
| 42 | 42 | tracing = "0.1" |
|
| 43 | 43 | tracing-subscriber = "0.3" |
|
| 44 | 44 | ||
| 45 | + | # Archive |
|
| 46 | + | zip = "2" |
|
| 47 | + | ||
| 45 | 48 | # Workspace crates |
|
| 46 | 49 | andromeda-auth = { path = "crates/auth" } |
| 24 | 24 | askama = "0.15" |
|
| 25 | 25 | askama_web = { version = "0.15", features = ["axum-0.8"] } |
|
| 26 | 26 | pulldown-cmark = "0.12" |
|
| 27 | + | zip = { workspace = true } |
| 278 | 278 | title: &str, |
|
| 279 | 279 | slug: &str, |
|
| 280 | 280 | content: &str, |
|
| 281 | + | status: &str, |
|
| 281 | 282 | alias: Option<&str>, |
|
| 282 | 283 | canonical_url: Option<&str>, |
|
| 283 | 284 | published_date: Option<&str>, |
|
| 287 | 288 | tags: Option<&str>, |
|
| 288 | 289 | ) -> Result<Option<Post>, DbError> { |
|
| 289 | 290 | let conn = db.lock().map_err(|_| DbError::LockPoisoned)?; |
|
| 291 | + | let effective_published_date = if status == "published" { |
|
| 292 | + | Some(published_date.unwrap_or("")) |
|
| 293 | + | } else { |
|
| 294 | + | published_date |
|
| 295 | + | }; |
|
| 290 | 296 | let rows = conn.execute( |
|
| 291 | - | "UPDATE posts SET title = ?1, slug = ?2, content = ?3, alias = ?4, canonical_url = ?5, |
|
| 292 | - | published_date = ?6, meta_description = ?7, meta_image = ?8, lang = ?9, tags = ?10, |
|
| 293 | - | updated_at = datetime('now') WHERE short_id = ?11", |
|
| 294 | - | params![title, slug, content, alias, canonical_url, published_date, meta_description, meta_image, lang, tags, short_id], |
|
| 297 | + | "UPDATE posts SET title = ?1, slug = ?2, content = ?3, status = ?4, alias = ?5, canonical_url = ?6, |
|
| 298 | + | published_date = CASE WHEN ?4 = 'published' THEN COALESCE(?7, published_date, datetime('now')) ELSE ?7 END, |
|
| 299 | + | meta_description = ?8, meta_image = ?9, lang = ?10, tags = ?11, |
|
| 300 | + | updated_at = datetime('now') WHERE short_id = ?12", |
|
| 301 | + | params![title, slug, content, status, alias, canonical_url, effective_published_date, meta_description, meta_image, lang, tags, short_id], |
|
| 295 | 302 | )?; |
|
| 296 | 303 | if rows == 0 { |
|
| 297 | 304 | return Ok(None); |
|
| 750 | 750 | attrs.slug.trim().to_string() |
|
| 751 | 751 | }; |
|
| 752 | 752 | ||
| 753 | + | let status = if form.action == "publish" { "published" } else { "draft" }; |
|
| 753 | 754 | let lang = if attrs.lang.trim().is_empty() { "en" } else { attrs.lang.trim() }; |
|
| 754 | 755 | let published_date = if attrs.published_date.trim().is_empty() { |
|
| 755 | - | now_datetime() |
|
| 756 | + | None |
|
| 756 | 757 | } else { |
|
| 757 | - | attrs.published_date.trim().to_string() |
|
| 758 | + | Some(attrs.published_date.trim().to_string()) |
|
| 758 | 759 | }; |
|
| 759 | 760 | ||
| 760 | 761 | match db::update_post( |
|
| 763 | 764 | title, |
|
| 764 | 765 | &slug, |
|
| 765 | 766 | &form.content, |
|
| 767 | + | status, |
|
| 766 | 768 | opt_str(&attrs.alias), |
|
| 767 | 769 | None, |
|
| 768 | - | Some(&published_date), |
|
| 770 | + | published_date.as_deref(), |
|
| 769 | 771 | opt_str(&attrs.meta_description), |
|
| 770 | 772 | opt_str(&attrs.meta_image), |
|
| 771 | 773 | lang, |
|
| 1189 | 1191 | .into_response() |
|
| 1190 | 1192 | } |
|
| 1191 | 1193 | ||
| 1194 | + | // --- Download/export handlers --- |
|
| 1195 | + | ||
| 1196 | + | async fn admin_download_posts( |
|
| 1197 | + | _session: auth::AuthSession, |
|
| 1198 | + | State(state): State<Arc<AppState>>, |
|
| 1199 | + | ) -> Response { |
|
| 1200 | + | let posts = match db::get_all_posts(&state.db) { |
|
| 1201 | + | Ok(posts) => posts, |
|
| 1202 | + | Err(e) => { |
|
| 1203 | + | tracing::error!("Failed to get posts for export: {}", e); |
|
| 1204 | + | return (StatusCode::INTERNAL_SERVER_ERROR, "Server error").into_response(); |
|
| 1205 | + | } |
|
| 1206 | + | }; |
|
| 1207 | + | ||
| 1208 | + | let result = tokio::task::spawn_blocking(move || { |
|
| 1209 | + | let mut buf = std::io::Cursor::new(Vec::new()); |
|
| 1210 | + | { |
|
| 1211 | + | let mut zip = zip::ZipWriter::new(&mut buf); |
|
| 1212 | + | let options = zip::write::SimpleFileOptions::default() |
|
| 1213 | + | .compression_method(zip::CompressionMethod::Deflated); |
|
| 1214 | + | for post in &posts { |
|
| 1215 | + | let filename = format!("{}.md", post.slug); |
|
| 1216 | + | let mut frontmatter = format!( |
|
| 1217 | + | "---\ntitle: {}\nslug: {}\nstatus: {}", |
|
| 1218 | + | post.title, post.slug, post.status |
|
| 1219 | + | ); |
|
| 1220 | + | if let Some(ref pd) = post.published_date { |
|
| 1221 | + | frontmatter.push_str(&format!("\npublished_date: {}", pd)); |
|
| 1222 | + | } |
|
| 1223 | + | if let Some(ref tags) = post.tags { |
|
| 1224 | + | frontmatter.push_str(&format!("\ntags: {}", tags)); |
|
| 1225 | + | } |
|
| 1226 | + | frontmatter.push_str(&format!("\nlang: {}", post.lang)); |
|
| 1227 | + | if let Some(ref alias) = post.alias { |
|
| 1228 | + | frontmatter.push_str(&format!("\nalias: {}", alias)); |
|
| 1229 | + | } |
|
| 1230 | + | if let Some(ref meta_image) = post.meta_image { |
|
| 1231 | + | frontmatter.push_str(&format!("\nmeta_image: {}", meta_image)); |
|
| 1232 | + | } |
|
| 1233 | + | if let Some(ref meta_desc) = post.meta_description { |
|
| 1234 | + | frontmatter.push_str(&format!("\ndescription: {}", meta_desc)); |
|
| 1235 | + | } |
|
| 1236 | + | frontmatter.push_str("\n---\n\n"); |
|
| 1237 | + | let content = format!("{}{}", frontmatter, post.content); |
|
| 1238 | + | if let Err(e) = zip.start_file(&filename, options) { |
|
| 1239 | + | tracing::warn!("Failed to add {} to zip: {}", filename, e); |
|
| 1240 | + | continue; |
|
| 1241 | + | } |
|
| 1242 | + | if let Err(e) = std::io::Write::write_all(&mut zip, content.as_bytes()) { |
|
| 1243 | + | tracing::warn!("Failed to write {} to zip: {}", filename, e); |
|
| 1244 | + | } |
|
| 1245 | + | } |
|
| 1246 | + | let _ = zip.finish(); |
|
| 1247 | + | } |
|
| 1248 | + | buf.into_inner() |
|
| 1249 | + | }) |
|
| 1250 | + | .await; |
|
| 1251 | + | ||
| 1252 | + | match result { |
|
| 1253 | + | Ok(bytes) => ( |
|
| 1254 | + | StatusCode::OK, |
|
| 1255 | + | [ |
|
| 1256 | + | (axum::http::header::CONTENT_TYPE, "application/zip"), |
|
| 1257 | + | ( |
|
| 1258 | + | axum::http::header::CONTENT_DISPOSITION, |
|
| 1259 | + | "attachment; filename=\"posts.zip\"", |
|
| 1260 | + | ), |
|
| 1261 | + | ], |
|
| 1262 | + | bytes, |
|
| 1263 | + | ) |
|
| 1264 | + | .into_response(), |
|
| 1265 | + | Err(e) => { |
|
| 1266 | + | tracing::error!("Failed to create posts zip: {}", e); |
|
| 1267 | + | (StatusCode::INTERNAL_SERVER_ERROR, "Export failed").into_response() |
|
| 1268 | + | } |
|
| 1269 | + | } |
|
| 1270 | + | } |
|
| 1271 | + | ||
| 1272 | + | async fn admin_download_uploads( |
|
| 1273 | + | _session: auth::AuthSession, |
|
| 1274 | + | State(state): State<Arc<AppState>>, |
|
| 1275 | + | ) -> Response { |
|
| 1276 | + | let files = match db::get_all_files(&state.db) { |
|
| 1277 | + | Ok(files) => files, |
|
| 1278 | + | Err(e) => { |
|
| 1279 | + | tracing::error!("Failed to get files for export: {}", e); |
|
| 1280 | + | return (StatusCode::INTERNAL_SERVER_ERROR, "Server error").into_response(); |
|
| 1281 | + | } |
|
| 1282 | + | }; |
|
| 1283 | + | ||
| 1284 | + | let uploads_dir = state.uploads_dir.clone(); |
|
| 1285 | + | let mut file_data: Vec<(String, Vec<u8>)> = Vec::new(); |
|
| 1286 | + | let mut seen_names = std::collections::HashSet::new(); |
|
| 1287 | + | for file in &files { |
|
| 1288 | + | let path = std::path::PathBuf::from(&uploads_dir).join(&file.filename); |
|
| 1289 | + | match tokio::fs::read(&path).await { |
|
| 1290 | + | Ok(bytes) => { |
|
| 1291 | + | let name = if seen_names.contains(&file.original_name) { |
|
| 1292 | + | format!("{}_{}", file.short_id, file.original_name) |
|
| 1293 | + | } else { |
|
| 1294 | + | file.original_name.clone() |
|
| 1295 | + | }; |
|
| 1296 | + | seen_names.insert(file.original_name.clone()); |
|
| 1297 | + | file_data.push((name, bytes)); |
|
| 1298 | + | } |
|
| 1299 | + | Err(e) => { |
|
| 1300 | + | tracing::warn!("Skipping file {} ({}): {}", file.original_name, file.filename, e); |
|
| 1301 | + | } |
|
| 1302 | + | } |
|
| 1303 | + | } |
|
| 1304 | + | ||
| 1305 | + | let result = tokio::task::spawn_blocking(move || { |
|
| 1306 | + | let mut buf = std::io::Cursor::new(Vec::new()); |
|
| 1307 | + | { |
|
| 1308 | + | let mut zip = zip::ZipWriter::new(&mut buf); |
|
| 1309 | + | let options = zip::write::SimpleFileOptions::default() |
|
| 1310 | + | .compression_method(zip::CompressionMethod::Stored); |
|
| 1311 | + | for (name, bytes) in &file_data { |
|
| 1312 | + | if let Err(e) = zip.start_file(name, options) { |
|
| 1313 | + | tracing::warn!("Failed to add {} to zip: {}", name, e); |
|
| 1314 | + | continue; |
|
| 1315 | + | } |
|
| 1316 | + | if let Err(e) = std::io::Write::write_all(&mut zip, bytes) { |
|
| 1317 | + | tracing::warn!("Failed to write {} to zip: {}", name, e); |
|
| 1318 | + | } |
|
| 1319 | + | } |
|
| 1320 | + | let _ = zip.finish(); |
|
| 1321 | + | } |
|
| 1322 | + | buf.into_inner() |
|
| 1323 | + | }) |
|
| 1324 | + | .await; |
|
| 1325 | + | ||
| 1326 | + | match result { |
|
| 1327 | + | Ok(bytes) => ( |
|
| 1328 | + | StatusCode::OK, |
|
| 1329 | + | [ |
|
| 1330 | + | (axum::http::header::CONTENT_TYPE, "application/zip"), |
|
| 1331 | + | ( |
|
| 1332 | + | axum::http::header::CONTENT_DISPOSITION, |
|
| 1333 | + | "attachment; filename=\"uploads.zip\"", |
|
| 1334 | + | ), |
|
| 1335 | + | ], |
|
| 1336 | + | bytes, |
|
| 1337 | + | ) |
|
| 1338 | + | .into_response(), |
|
| 1339 | + | Err(e) => { |
|
| 1340 | + | tracing::error!("Failed to create uploads zip: {}", e); |
|
| 1341 | + | (StatusCode::INTERNAL_SERVER_ERROR, "Export failed").into_response() |
|
| 1342 | + | } |
|
| 1343 | + | } |
|
| 1344 | + | } |
|
| 1345 | + | ||
| 1192 | 1346 | // --- Date helper --- |
|
| 1193 | 1347 | ||
| 1194 | 1348 | fn days_to_ymd(mut days: i64) -> (i64, i64, i64) { |
|
| 1271 | 1425 | .route("/admin/pages/{id}/delete", post(admin_delete_page)) |
|
| 1272 | 1426 | // Admin settings |
|
| 1273 | 1427 | .route("/admin/settings", get(admin_get_settings).post(admin_post_settings)) |
|
| 1428 | + | // Admin downloads |
|
| 1429 | + | .route("/admin/downloads/posts", get(admin_download_posts)) |
|
| 1430 | + | .route("/admin/downloads/uploads", get(admin_download_uploads)) |
|
| 1274 | 1431 | // Admin files |
|
| 1275 | 1432 | .route("/admin/files", get(admin_files)) |
|
| 1276 | 1433 | .route("/admin/files/upload", post(admin_upload_file)) |
|
| 686 | 686 | background: #ffffff; |
|
| 687 | 687 | } |
|
| 688 | 688 | ||
| 689 | + | /* File thumbnails */ |
|
| 690 | + | ||
| 691 | + | .file-thumbnail { |
|
| 692 | + | max-width: 60px; |
|
| 693 | + | max-height: 60px; |
|
| 694 | + | object-fit: cover; |
|
| 695 | + | border: 1px solid #333; |
|
| 696 | + | flex-shrink: 0; |
|
| 697 | + | } |
|
| 698 | + | ||
| 699 | + | /* Footer */ |
|
| 700 | + | ||
| 701 | + | .footer { |
|
| 702 | + | width: 100%; |
|
| 703 | + | border-top: 1px solid #333; |
|
| 704 | + | padding-top: 1rem; |
|
| 705 | + | margin-top: auto; |
|
| 706 | + | display: flex; |
|
| 707 | + | justify-content: center; |
|
| 708 | + | } |
|
| 709 | + | ||
| 710 | + | .rss-link { |
|
| 711 | + | display: flex; |
|
| 712 | + | align-items: center; |
|
| 713 | + | gap: 0.4rem; |
|
| 714 | + | font-size: 12px; |
|
| 715 | + | opacity: 0.5; |
|
| 716 | + | } |
|
| 717 | + | ||
| 718 | + | .rss-link:hover { |
|
| 719 | + | opacity: 0.8; |
|
| 720 | + | } |
|
| 721 | + | ||
| 722 | + | /* Export buttons */ |
|
| 723 | + | ||
| 724 | + | a.btn { |
|
| 725 | + | display: inline-block; |
|
| 726 | + | background: #121113; |
|
| 727 | + | color: #ffffff; |
|
| 728 | + | border: 1px solid white; |
|
| 729 | + | padding: 0.4rem 0.75rem; |
|
| 730 | + | font-size: 14px; |
|
| 731 | + | font-family: "Commit Mono", monospace; |
|
| 732 | + | text-decoration: none; |
|
| 733 | + | cursor: pointer; |
|
| 734 | + | } |
|
| 735 | + | ||
| 736 | + | a.btn:hover { |
|
| 737 | + | opacity: 0.7; |
|
| 738 | + | } |
|
| 739 | + | ||
| 689 | 740 | .hidden { |
|
| 690 | 741 | display: none; |
|
| 691 | 742 | } |
| 21 | 21 | <div class="admin-list"> |
|
| 22 | 22 | {% for file in files %} |
|
| 23 | 23 | <div class="admin-list-item"> |
|
| 24 | + | {% if file.content_type.starts_with("image/") %} |
|
| 25 | + | <img src="/files/{{ file.filename }}" class="file-thumbnail" alt="{{ file.original_name }}"> |
|
| 26 | + | {% endif %} |
|
| 24 | 27 | <div class="admin-list-info"> |
|
| 25 | 28 | <span class="admin-list-title">{{ file.original_name }}</span> |
|
| 26 | 29 | <div class="admin-list-meta"> |
| 44 | 44 | <label for="content">content</label> |
|
| 45 | 45 | <textarea id="content" name="content" class="post-content">{{ p.content }}</textarea> |
|
| 46 | 46 | <div class="form-actions"> |
|
| 47 | - | <button type="submit" name="action" value="draft">save draft</button> |
|
| 48 | - | <button type="submit" name="action" value="publish">publish</button> |
|
| 47 | + | {% if p.status == "published" %} |
|
| 48 | + | <button type="submit" name="action" value="publish">update</button> |
|
| 49 | + | <button type="submit" name="action" value="draft">unpublish</button> |
|
| 50 | + | {% else %} |
|
| 51 | + | <button type="submit" name="action" value="draft">save draft</button> |
|
| 52 | + | <button type="submit" name="action" value="publish">publish</button> |
|
| 53 | + | {% endif %} |
|
| 49 | 54 | </div> |
|
| 50 | 55 | </form> |
|
| 51 | 56 | {% when None %} |
| 31 | 31 | </div> |
|
| 32 | 32 | <button type="submit">save</button> |
|
| 33 | 33 | </form> |
|
| 34 | + | <h3>Data Export</h3> |
|
| 35 | + | <div class="form-actions"> |
|
| 36 | + | <a href="/admin/downloads/posts" class="btn">download posts</a> |
|
| 37 | + | <a href="/admin/downloads/uploads" class="btn">download uploads</a> |
|
| 38 | + | </div> |
|
| 34 | 39 | <script> |
|
| 35 | 40 | var toggle = document.getElementById('custom_css_toggle'); |
|
| 36 | 41 | var section = document.getElementById('custom_css_section'); |
| 31 | 31 | <main> |
|
| 32 | 32 | {% block content %}{% endblock %} |
|
| 33 | 33 | </main> |
|
| 34 | + | <footer class="footer"> |
|
| 35 | + | <a href="/feed.xml" class="rss-link" title="RSS Feed"> |
|
| 36 | + | <svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="currentColor"> |
|
| 37 | + | <circle cx="3.429" cy="20.571" r="3.429"/> |
|
| 38 | + | <path d="M11.429 24h4.57C15.999 15.179 8.821 8.001 0 8.001v4.57c6.297 0 11.429 5.132 11.429 11.429z"/> |
|
| 39 | + | <path d="M19.999 24C19.999 10.767 9.233 0 0 0v4.571c10.714 0 15.428 8.714 15.428 19.429h4.571z"/> |
|
| 40 | + | </svg> |
|
| 41 | + | RSS |
|
| 42 | + | </a> |
|
| 43 | + | </footer> |
|
| 34 | 44 | </body> |
|
| 35 | 45 | </html> |