+
+ Click on the Vite and React logos to learn more
+
+ >
+ )
+}
+
+export default App
diff --git a/apps/client/src/assets/react.svg b/apps/client/src/assets/react.svg
new file mode 100644
index 00000000..6c87de9b
--- /dev/null
+++ b/apps/client/src/assets/react.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/apps/client/src/index.css b/apps/client/src/index.css
new file mode 100644
index 00000000..6119ad9a
--- /dev/null
+++ b/apps/client/src/index.css
@@ -0,0 +1,68 @@
+:root {
+ font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
+ line-height: 1.5;
+ font-weight: 400;
+
+ color-scheme: light dark;
+ color: rgba(255, 255, 255, 0.87);
+ background-color: #242424;
+
+ font-synthesis: none;
+ text-rendering: optimizeLegibility;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+}
+
+a {
+ font-weight: 500;
+ color: #646cff;
+ text-decoration: inherit;
+}
+a:hover {
+ color: #535bf2;
+}
+
+body {
+ margin: 0;
+ display: flex;
+ place-items: center;
+ min-width: 320px;
+ min-height: 100vh;
+}
+
+h1 {
+ font-size: 3.2em;
+ line-height: 1.1;
+}
+
+button {
+ border-radius: 8px;
+ border: 1px solid transparent;
+ padding: 0.6em 1.2em;
+ font-size: 1em;
+ font-weight: 500;
+ font-family: inherit;
+ background-color: #1a1a1a;
+ cursor: pointer;
+ transition: border-color 0.25s;
+}
+button:hover {
+ border-color: #646cff;
+}
+button:focus,
+button:focus-visible {
+ outline: 4px auto -webkit-focus-ring-color;
+}
+
+@media (prefers-color-scheme: light) {
+ :root {
+ color: #213547;
+ background-color: #ffffff;
+ }
+ a:hover {
+ color: #747bff;
+ }
+ button {
+ background-color: #f9f9f9;
+ }
+}
diff --git a/apps/client/src/main.tsx b/apps/client/src/main.tsx
new file mode 100644
index 00000000..6f4ac9bc
--- /dev/null
+++ b/apps/client/src/main.tsx
@@ -0,0 +1,10 @@
+import { StrictMode } from 'react'
+import { createRoot } from 'react-dom/client'
+import App from './App.tsx'
+import './index.css'
+
+createRoot(document.getElementById('root')!).render(
+
+
+ ,
+)
diff --git a/apps/client/src/routeTree.gen.ts b/apps/client/src/routeTree.gen.ts
new file mode 100644
index 00000000..d6919f5f
--- /dev/null
+++ b/apps/client/src/routeTree.gen.ts
@@ -0,0 +1,116 @@
+/* eslint-disable */
+
+// @ts-nocheck
+
+// noinspection JSUnusedGlobalSymbols
+
+// This file was automatically generated by TanStack Router.
+// You should NOT make any changes in this file as it will be overwritten.
+// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
+
+import { createFileRoute } from '@tanstack/react-router'
+
+// Import Routes
+
+import { Route as rootRoute } from './routes/__root'
+
+// Create Virtual Routes
+
+const IndexLazyImport = createFileRoute('/')()
+const InnIndexLazyImport = createFileRoute('/inn/')()
+
+// Create/Update Routes
+
+const IndexLazyRoute = IndexLazyImport.update({
+ id: '/',
+ path: '/',
+ getParentRoute: () => rootRoute,
+} as any).lazy(() => import('./routes/index.lazy').then((d) => d.Route))
+
+const InnIndexLazyRoute = InnIndexLazyImport.update({
+ id: '/inn/',
+ path: '/inn/',
+ getParentRoute: () => rootRoute,
+} as any).lazy(() => import('./routes/inn.index.lazy').then((d) => d.Route))
+
+// Populate the FileRoutesByPath interface
+
+declare module '@tanstack/react-router' {
+ interface FileRoutesByPath {
+ '/': {
+ id: '/'
+ path: '/'
+ fullPath: '/'
+ preLoaderRoute: typeof IndexLazyImport
+ parentRoute: typeof rootRoute
+ }
+ '/inn/': {
+ id: '/inn/'
+ path: '/inn'
+ fullPath: '/inn'
+ preLoaderRoute: typeof InnIndexLazyImport
+ parentRoute: typeof rootRoute
+ }
+ }
+}
+
+// Create and export the route tree
+
+export interface FileRoutesByFullPath {
+ '/': typeof IndexLazyRoute
+ '/inn': typeof InnIndexLazyRoute
+}
+
+export interface FileRoutesByTo {
+ '/': typeof IndexLazyRoute
+ '/inn': typeof InnIndexLazyRoute
+}
+
+export interface FileRoutesById {
+ __root__: typeof rootRoute
+ '/': typeof IndexLazyRoute
+ '/inn/': typeof InnIndexLazyRoute
+}
+
+export interface FileRouteTypes {
+ fileRoutesByFullPath: FileRoutesByFullPath
+ fullPaths: '/' | '/inn'
+ fileRoutesByTo: FileRoutesByTo
+ to: '/' | '/inn'
+ id: '__root__' | '/' | '/inn/'
+ fileRoutesById: FileRoutesById
+}
+
+export interface RootRouteChildren {
+ IndexLazyRoute: typeof IndexLazyRoute
+ InnIndexLazyRoute: typeof InnIndexLazyRoute
+}
+
+const rootRouteChildren: RootRouteChildren = {
+ IndexLazyRoute: IndexLazyRoute,
+ InnIndexLazyRoute: InnIndexLazyRoute,
+}
+
+export const routeTree = rootRoute
+ ._addFileChildren(rootRouteChildren)
+ ._addFileTypes()
+
+/* ROUTE_MANIFEST_START
+{
+ "routes": {
+ "__root__": {
+ "filePath": "__root.tsx",
+ "children": [
+ "/",
+ "/inn/"
+ ]
+ },
+ "/": {
+ "filePath": "index.lazy.tsx"
+ },
+ "/inn/": {
+ "filePath": "inn.index.lazy.tsx"
+ }
+ }
+}
+ROUTE_MANIFEST_END */
diff --git a/apps/client/src/routes/__root.tsx b/apps/client/src/routes/__root.tsx
new file mode 100644
index 00000000..5700ddd7
--- /dev/null
+++ b/apps/client/src/routes/__root.tsx
@@ -0,0 +1,20 @@
+import { createRootRoute, Link, Outlet } from '@tanstack/react-router'
+import { TanStackRouterDevtools } from '@tanstack/router-devtools'
+
+export const Route = createRootRoute({
+ component: () => (
+ <>
+
+
+ Home
+ {' '}
+
+ Inn
+
+
+
+
+
+ >
+ ),
+})
diff --git a/apps/client/src/routes/index.lazy.tsx b/apps/client/src/routes/index.lazy.tsx
new file mode 100644
index 00000000..f8f08fe8
--- /dev/null
+++ b/apps/client/src/routes/index.lazy.tsx
@@ -0,0 +1,13 @@
+import { createLazyFileRoute } from '@tanstack/react-router'
+
+export const Route = createLazyFileRoute('/')({
+ component: Index,
+})
+
+function Index() {
+ return (
+
+
Welcome Home!
+
+ )
+}
diff --git a/apps/client/src/routes/inn.index.lazy.tsx b/apps/client/src/routes/inn.index.lazy.tsx
new file mode 100644
index 00000000..fc1a7353
--- /dev/null
+++ b/apps/client/src/routes/inn.index.lazy.tsx
@@ -0,0 +1,28 @@
+import { createLazyFileRoute } from '@tanstack/react-router'
+import { useEffect, useState } from "react";
+
+export const Route = createLazyFileRoute('/inn/')({
+ component: InnsList,
+})
+
+function InnsList() {
+ const [htmlContent, setHtmlContent] = useState(null);
+
+useEffect(() => {
+ // Fetch raw HTML from the API
+ fetch("http://localhost:3001/inn/0")
+ .then((response) => response.text())
+ .then((html) => setHtmlContent(html))
+ .catch((error) => console.error("Failed to fetch HTML:", error));
+ }, []);
+
+ if (!htmlContent) {
+ return Loading...
;
+ }
+
+ return (
+
+ )
+}
diff --git a/apps/client/src/vite-env.d.ts b/apps/client/src/vite-env.d.ts
new file mode 100644
index 00000000..11f02fe2
--- /dev/null
+++ b/apps/client/src/vite-env.d.ts
@@ -0,0 +1 @@
+///
diff --git a/apps/client/tsconfig.app.json b/apps/client/tsconfig.app.json
new file mode 100644
index 00000000..f0a23505
--- /dev/null
+++ b/apps/client/tsconfig.app.json
@@ -0,0 +1,24 @@
+{
+ "compilerOptions": {
+ "target": "ES2020",
+ "useDefineForClassFields": true,
+ "lib": ["ES2020", "DOM", "DOM.Iterable"],
+ "module": "ESNext",
+ "skipLibCheck": true,
+
+ /* Bundler mode */
+ "moduleResolution": "bundler",
+ "allowImportingTsExtensions": true,
+ "isolatedModules": true,
+ "moduleDetection": "force",
+ "noEmit": true,
+ "jsx": "react-jsx",
+
+ /* Linting */
+ "strict": true,
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+ "noFallthroughCasesInSwitch": true
+ },
+ "include": ["src"]
+}
diff --git a/apps/client/tsconfig.json b/apps/client/tsconfig.json
new file mode 100644
index 00000000..1ffef600
--- /dev/null
+++ b/apps/client/tsconfig.json
@@ -0,0 +1,7 @@
+{
+ "files": [],
+ "references": [
+ { "path": "./tsconfig.app.json" },
+ { "path": "./tsconfig.node.json" }
+ ]
+}
diff --git a/apps/client/tsconfig.node.json b/apps/client/tsconfig.node.json
new file mode 100644
index 00000000..0d3d7144
--- /dev/null
+++ b/apps/client/tsconfig.node.json
@@ -0,0 +1,22 @@
+{
+ "compilerOptions": {
+ "target": "ES2022",
+ "lib": ["ES2023"],
+ "module": "ESNext",
+ "skipLibCheck": true,
+
+ /* Bundler mode */
+ "moduleResolution": "bundler",
+ "allowImportingTsExtensions": true,
+ "isolatedModules": true,
+ "moduleDetection": "force",
+ "noEmit": true,
+
+ /* Linting */
+ "strict": true,
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+ "noFallthroughCasesInSwitch": true
+ },
+ "include": ["vite.config.ts"]
+}
diff --git a/apps/client/vite.config.ts b/apps/client/vite.config.ts
new file mode 100644
index 00000000..160a8e73
--- /dev/null
+++ b/apps/client/vite.config.ts
@@ -0,0 +1,10 @@
+import { defineConfig } from 'vite'
+import react from '@vitejs/plugin-react-swc'
+import { TanStackRouterVite } from '@tanstack/router-plugin/vite'
+
+// https://vitejs.dev/config/
+export default defineConfig({
+ plugins: [
+ TanStackRouterVite(),
+ react()],
+})
diff --git a/apps/server/.cargo/config.toml b/apps/server/.cargo/config.toml
new file mode 100644
index 00000000..b6e2773a
--- /dev/null
+++ b/apps/server/.cargo/config.toml
@@ -0,0 +1,2 @@
+[build]
+target-dir = "../../apps/server/target"
diff --git a/apps/server/Cargo.toml b/apps/server/Cargo.toml
new file mode 100644
index 00000000..2e9457d5
--- /dev/null
+++ b/apps/server/Cargo.toml
@@ -0,0 +1,54 @@
+[package]
+edition = "2021"
+name = "freedit"
+version = "0.7.5"
+
+[dependencies]
+ammonia = "4.0.0"
+atom_syndication = { version = "0.12", default-features = false }
+axum = { version = "0.7.5", features = ["http1", "http2", "form", "query", "multipart", "tokio"], default-features = false }
+axum-extra = { version = "0.9", features = ["typed-header"] }
+axum_garde = { version = "0.20.0", default-features = false, features = ["form"] }
+basic-toml = "*"
+bincode = "2.0.0-rc.3"
+cached = { version = "0.54.0", default-features = false, features = ["proc_macro", "ahash"] }
+captcha = { git = "https://github.com/freedit-dev/captcha.git", default-features = false }
+data-encoding = "*"
+fast2s = "0.3"
+garde = { version = "0.20.0", features = ["derive"] }
+http = "1.1"
+identicon = { git = "https://github.com/freedit-dev/identicon.git", default-features = false }
+image = { version = "0.25.2", default-features = false, features = ["jpeg", "png", "gif"] }
+img-parts = "0.3.0"
+indexmap = "2"
+jieba-rs = { git = "https://github.com/messense/jieba-rs.git", rev = "b39957e" }
+jiff = { version = "0.1.13", default-features = false, features = ["std"] }
+latex2mathml = "0.2.3"
+mozjpeg = "0.10.10"
+nanoid = "0.4.0"
+pulldown-cmark = { version = "0.12.0", features = ["simd", "html"], default-features = false }
+rand = "0.8"
+regex = "1"
+reqwest = { version = "0.12", default-features = false, features = ["rustls-tls", "socks"] }
+ring = { version = "0.17", default-features = false }
+rinja = { version = "0.3.4", default-features = false }
+rinja_axum = { version = "0.3.4", default-features = false }
+rss = { version = "2.0", default-features = false }
+rust-stemmers = "1.2.0"
+serde = { version = "1.0", features = ["derive"] }
+sled = "0.34.7"
+snailquote = "0.3.1"
+stop-words = "0.8.0"
+syntect = { version = "5", features = ["regex-fancy", "default-syntaxes", "default-themes", "html"], default-features = false }
+tantivy = "0.22.0"
+thiserror = "2"
+tokio = { version = "1", features = ["macros", "rt-multi-thread"] }
+tower = { version = "0.5.1", features = ["timeout"] }
+tower-http = { version = "0.6.1", features = ["fs", "compression-zstd", "cors", "trace"] }
+tracing = { version = "0.1", features = ["release_max_level_info", "max_level_info"], default-features = false }
+tracing-subscriber = { version = "0.3", features = ["env-filter", "fmt", "smallvec"], default-features = false }
+unicode-segmentation = "1"
+whichlang = "0.1.0"
+
+[target.'cfg(not(target_os = "windows"))'.dependencies]
+tikv-jemallocator = "0.6"
diff --git a/apps/server/README.md b/apps/server/README.md
new file mode 100644
index 00000000..c6a69201
--- /dev/null
+++ b/apps/server/README.md
@@ -0,0 +1,70 @@
+# freedit
+
+[![CI](https://github.com/freedit-org/freedit/actions/workflows/ci.yml/badge.svg)](https://github.com/freedit-org/freedit/actions/workflows/ci.yml)
+[![release](https://github.com/freedit-org/freedit/actions/workflows/release.yml/badge.svg)](https://github.com/freedit-org/freedit/releases)
+[![Doc](https://img.shields.io/github/deployments/freedit-org/freedit/github-pages?label=doc)](https://freedit-org.github.io/freedit/freedit/index.html)
+
+The safest and lightest forum, powered by rust.
+
+Demo:
+
+GitHub:
+
+## Support
+
+Help support the development and maintenance of freedit. Your contributions are greatly appreciated!
+
+- Monero (XMR): `45JB1KbCM54gw7zDY8LzkDXjEibDgTspyKBzM8VWi8mL1gY3wCyzHsCSRGRsXBwGgdC6HX1EtJFoNYXZELnDQW8S7DRG8tL`
+
+All donations go towards hosting costs and continued development of freedit. Thank you for your support!
+
+## Features
+
+* Easy to deploy: one binary to run, using embedded database [sled](https://github.com/spacejam/sled)
+* No javascript at all, for safety maximization. ([Why javascript is evil](https://thehackernews.com/2022/05/tails-os-users-advised-not-to-use-tor.html))
+* e2ee private message
+* Math and Code highlighting support without JavaScript
+* Markdown support
+* inn: Subgroup like Subreddits
+* solo: Personal space like Twitter
+* Online rss reader
+
+## Usage
+
+### From binary
+
+1. Download freedit binary from [releases](https://github.com/freedit-org/freedit/releases)
+2. unzip freedit.zip
+3. run `./freedit`, open browser to `addr`,
+
+### From source code
+
+Prerequisition: install [Rust](https://www.rust-lang.org/tools/install)
+
+```bash
+git clone https://github.com/freedit-org/freedit
+cd freedit && cargo build -r
+./target/release/freedit
+```
+
+## Documentation
+
+* online doc:
+
+* generate local documentation:
+```bash
+cargo doc --no-deps --open
+```
+
+## Development
+
+```bash
+git clone https://github.com/freedit-org/freedit
+cd freedit && cargo run
+```
+
+## Credits
+
+* icon:
+* CSS framework:
+* Rust crates: [Cargo.toml](https://github.com/freedit-org/freedit/blob/main/Cargo.toml)
\ No newline at end of file
diff --git a/build.rs b/apps/server/build.rs
similarity index 100%
rename from build.rs
rename to apps/server/build.rs
diff --git a/i18n/en.toml b/apps/server/i18n/en.toml
similarity index 100%
rename from i18n/en.toml
rename to apps/server/i18n/en.toml
diff --git a/i18n/fr.toml b/apps/server/i18n/fr.toml
similarity index 100%
rename from i18n/fr.toml
rename to apps/server/i18n/fr.toml
diff --git a/i18n/ja.toml b/apps/server/i18n/ja.toml
similarity index 100%
rename from i18n/ja.toml
rename to apps/server/i18n/ja.toml
diff --git a/i18n/zh_cn.toml b/apps/server/i18n/zh_cn.toml
similarity index 100%
rename from i18n/zh_cn.toml
rename to apps/server/i18n/zh_cn.toml
diff --git a/src/app_router.rs b/apps/server/src/app_router.rs
similarity index 94%
rename from src/app_router.rs
rename to apps/server/src/app_router.rs
index 40977d40..80f8378a 100644
--- a/src/app_router.rs
+++ b/apps/server/src/app_router.rs
@@ -26,10 +26,12 @@ use axum::{
error_handling::HandleErrorLayer, extract::DefaultBodyLimit, handler::Handler,
http::StatusCode, routing::get, BoxError, Router,
};
+use http::header::HeaderValue;
use std::time::Duration;
use tower::{timeout::TimeoutLayer, ServiceBuilder};
use tower_http::{
compression::CompressionLayer,
+ cors::{Any, CorsLayer},
services::ServeDir,
trace::{DefaultMakeSpan, TraceLayer},
};
@@ -38,15 +40,19 @@ use tracing::Level;
const UPLOAD_LIMIT: usize = 20 * 1024 * 1024;
pub async fn router() -> Router {
+ let cors = CorsLayer::new()
+ .allow_origin(HeaderValue::from_static("http://localhost:5173"))
+ .allow_methods(Any)
+ .allow_headers(Any);
+
let middleware_stack = ServiceBuilder::new()
.layer(HandleErrorLayer::new(|_: BoxError| async {
StatusCode::REQUEST_TIMEOUT
}))
.layer(TimeoutLayer::new(Duration::from_secs(10)))
.layer(CompressionLayer::new())
- .layer(
- TraceLayer::new_for_http().make_span_with(DefaultMakeSpan::new().level(Level::INFO)),
- );
+ .layer(TraceLayer::new_for_http().make_span_with(DefaultMakeSpan::new().level(Level::INFO)))
+ .layer(cors);
let router_db = Router::new()
.route("/", get(home))
diff --git a/apps/server/src/config.rs b/apps/server/src/config.rs
new file mode 100644
index 00000000..fed3a187
--- /dev/null
+++ b/apps/server/src/config.rs
@@ -0,0 +1,130 @@
+use serde::{Deserialize, Serialize};
+use std::env;
+use std::fs::{self, read_to_string, File};
+use std::io::Write;
+use std::path::{Path, PathBuf};
+use std::sync::LazyLock;
+use tracing::{info, warn};
+
+pub static CONFIG: LazyLock = LazyLock::new(Config::load_config);
+
+#[derive(Serialize, Deserialize)]
+pub struct Config {
+ pub db: PathBuf,
+ pub snapshots_path: PathBuf,
+ pub addr: String,
+ pub rebuild_index: Option,
+ pub(crate) avatars_path: PathBuf,
+ pub(crate) inn_icons_path: PathBuf,
+ pub(crate) upload_path: PathBuf,
+ pub(crate) tantivy_path: PathBuf,
+ pub(crate) proxy: String,
+}
+
+impl Config {
+ fn load_config() -> Config {
+ let exe_path = env::current_exe().expect("Failed to get current executable path");
+ let exe_dir = exe_path
+ .parent()
+ .expect("Fialed to get executable directory")
+ .parent()
+ .expect("Failed to get target directory")
+ .parent()
+ .expect("Failed to get server directory");
+
+ let cfg_file = exe_dir.join(
+ env::args()
+ .nth(1)
+ .unwrap_or_else(|| "config.toml".to_owned()),
+ );
+ let config = if let Ok(config_toml_content) = read_to_string(&cfg_file) {
+ let mut config: Config =
+ basic_toml::from_str(&config_toml_content).expect("Failed to parse config.toml");
+ config.resolve_paths(&exe_dir);
+ config
+ } else {
+ warn!("Config file not found, using default config.toml");
+ let mut config = Config::default();
+ config.resolve_paths(&exe_dir);
+ let toml = basic_toml::to_string(&config).expect("Failed to serialize config.toml");
+ let mut file = File::create(&cfg_file).expect("Failed to create config.toml file");
+ file.write_all(toml.as_bytes())
+ .expect("Failed to write to config.toml");
+ info!("Wrote default config file at {}", &cfg_file.display());
+ config
+ };
+
+ config.ensure_dirs();
+ config
+ }
+
+ fn resolve_paths(&mut self, base_dir: &Path) {
+ let path_fields: &mut [&mut PathBuf] = &mut [
+ &mut self.db,
+ &mut self.snapshots_path,
+ &mut self.avatars_path,
+ &mut self.inn_icons_path,
+ &mut self.upload_path,
+ &mut self.tantivy_path,
+ ];
+
+ for p in path_fields.iter_mut() {
+ **p = resolve_path(base_dir, p.as_path());
+ }
+ }
+
+ fn ensure_dirs(&self) {
+ let path_fields = [
+ &self.db,
+ &self.snapshots_path,
+ &self.avatars_path,
+ &self.inn_icons_path,
+ &self.upload_path,
+ &self.tantivy_path,
+ ];
+
+ for path in &path_fields {
+ check_path(path);
+ }
+ }
+}
+
+impl Default for Config {
+ fn default() -> Self {
+ Config {
+ db: PathBuf::from("freedit.db"),
+ snapshots_path: PathBuf::from("snapshots"),
+ addr: "127.0.0.1:3001".into(),
+ rebuild_index: None,
+ avatars_path: PathBuf::from("static/imgs/avatars"),
+ inn_icons_path: PathBuf::from("static/imgs/inn_icons"),
+ upload_path: PathBuf::from("static/imgs/upload"),
+ tantivy_path: PathBuf::from("tantivy"),
+ proxy: "".into(),
+ }
+ }
+}
+
+/// Resolve a PathBuf relative to base_dir if it's not absolute
+fn resolve_path(base_dir: &Path, path: &Path) -> PathBuf {
+ if path.is_absolute() {
+ path.to_path_buf()
+ } else {
+ base_dir.join(path)
+ }
+}
+
+/// Create new dir if the path doesn't exist.
+fn check_path(path: &Path) {
+ if !path.exists() {
+ fs::create_dir_all(path).unwrap_or_else(|_| {
+ panic!(
+ "Failed to created necessary dir at {:?}",
+ path.canonicalize().unwrap_or_else(|_| path.to_path_buf())
+ )
+ });
+ info!("Created dir: {:?}", path);
+ } else {
+ info!("Dir already exists {:?}", path);
+ }
+}
diff --git a/src/controller/admin.rs b/apps/server/src/controller/admin.rs
similarity index 100%
rename from src/controller/admin.rs
rename to apps/server/src/controller/admin.rs
diff --git a/src/controller/db_utils.rs b/apps/server/src/controller/db_utils.rs
similarity index 100%
rename from src/controller/db_utils.rs
rename to apps/server/src/controller/db_utils.rs
diff --git a/src/controller/feed.rs b/apps/server/src/controller/feed.rs
similarity index 100%
rename from src/controller/feed.rs
rename to apps/server/src/controller/feed.rs
diff --git a/src/controller/fmt.rs b/apps/server/src/controller/fmt.rs
similarity index 100%
rename from src/controller/fmt.rs
rename to apps/server/src/controller/fmt.rs
diff --git a/src/controller/inn.rs b/apps/server/src/controller/inn.rs
similarity index 100%
rename from src/controller/inn.rs
rename to apps/server/src/controller/inn.rs
diff --git a/src/controller/message.rs b/apps/server/src/controller/message.rs
similarity index 100%
rename from src/controller/message.rs
rename to apps/server/src/controller/message.rs
diff --git a/src/controller/meta_handler.rs b/apps/server/src/controller/meta_handler.rs
similarity index 100%
rename from src/controller/meta_handler.rs
rename to apps/server/src/controller/meta_handler.rs
diff --git a/src/controller/mod.rs b/apps/server/src/controller/mod.rs
similarity index 100%
rename from src/controller/mod.rs
rename to apps/server/src/controller/mod.rs
diff --git a/src/controller/notification.rs b/apps/server/src/controller/notification.rs
similarity index 100%
rename from src/controller/notification.rs
rename to apps/server/src/controller/notification.rs
diff --git a/src/controller/solo.rs b/apps/server/src/controller/solo.rs
similarity index 100%
rename from src/controller/solo.rs
rename to apps/server/src/controller/solo.rs
diff --git a/src/controller/tantivy.rs b/apps/server/src/controller/tantivy.rs
similarity index 100%
rename from src/controller/tantivy.rs
rename to apps/server/src/controller/tantivy.rs
diff --git a/src/controller/upload.rs b/apps/server/src/controller/upload.rs
similarity index 97%
rename from src/controller/upload.rs
rename to apps/server/src/controller/upload.rs
index 7255b922..0e773b2f 100644
--- a/src/controller/upload.rs
+++ b/apps/server/src/controller/upload.rs
@@ -53,14 +53,14 @@ pub(crate) async fn upload_pic_post(
return Err(AppError::Unauthorized);
}
target = format!("/mod/{iid}");
- format!("{}/{}.png", &CONFIG.inn_icons_path, iid)
+ format!("{}/{}.png", &CONFIG.inn_icons_path.display(), iid)
} else {
return Err(AppError::NotFound);
}
}
"user" => {
target = "/user/setting".to_string();
- format!("{}/{}.png", &CONFIG.avatars_path, claim.uid)
+ format!("{}/{}.png", &CONFIG.avatars_path.display(), claim.uid)
}
_ => unreachable!(),
};
@@ -177,7 +177,7 @@ pub(crate) async fn image_delete(
if count == 0 {
let img = String::from_utf8_lossy(&v1);
- let path = format!("{}/{}", CONFIG.upload_path, img);
+ let path = format!("{}/{}", CONFIG.upload_path.display(), img);
remove_file(path).await?;
}
} else {
@@ -315,7 +315,7 @@ pub(crate) async fn upload_post(
let digest = context.finish();
let sha1 = HEXLOWER.encode(digest.as_ref());
let fname = format!("{}.{}", &sha1[0..20], ext);
- let location = format!("{}/{}", &CONFIG.upload_path, fname);
+ let location = format!("{}/{}", &CONFIG.upload_path.display(), fname);
fs::write(location, &img_data).await.unwrap();
let img_id = incr_id(&DB, "imgs_count")?;
diff --git a/src/controller/user.rs b/apps/server/src/controller/user.rs
similarity index 99%
rename from src/controller/user.rs
rename to apps/server/src/controller/user.rs
index a233920a..bf984e50 100644
--- a/src/controller/user.rs
+++ b/apps/server/src/controller/user.rs
@@ -987,7 +987,7 @@ pub(crate) async fn signup_post(
let password_hash = generate_password_hash(&input.password);
let uid = incr_id(&DB, "users_count")?;
- let avatar = format!("{}/{}.png", &CONFIG.avatars_path, uid);
+ let avatar = format!("{}/{}.png", &CONFIG.avatars_path.display(), uid);
Identicon::new(&generate_salt()).image().save(avatar)?;
let created_at = Timestamp::now().as_second();
diff --git a/src/error.rs b/apps/server/src/error.rs
similarity index 100%
rename from src/error.rs
rename to apps/server/src/error.rs
diff --git a/src/lib.rs b/apps/server/src/lib.rs
similarity index 97%
rename from src/lib.rs
rename to apps/server/src/lib.rs
index 14559bff..cfc65bcc 100644
--- a/src/lib.rs
+++ b/apps/server/src/lib.rs
@@ -58,6 +58,6 @@ pub static DB: LazyLock = LazyLock::new(|| {
let db_url = &CONFIG.db;
let config = sled::Config::default().path(db_url);
let db = config.open().unwrap();
- info!(%db_url);
+ info!("{}", db_url.display());
db
});
diff --git a/src/main.rs b/apps/server/src/main.rs
similarity index 92%
rename from src/main.rs
rename to apps/server/src/main.rs
index 879d4a5c..fc14af93 100644
--- a/src/main.rs
+++ b/apps/server/src/main.rs
@@ -29,12 +29,7 @@ async fn main() -> Result<(), AppError> {
#[cfg(not(debug_assertions))]
tokio::spawn(async move {
loop {
- let snapshot_path = PathBuf::from("snapshots");
- // create snapshot dir if needed
- if !snapshot_path.exists() {
- fs::create_dir_all(&snapshot_path).unwrap();
- }
- // create a snapshot
+ let snapshot_path = &CONFIG.snapshots_path;
create_snapshot(&snapshot_path, &DB);
// remove snapshots older than 48 hours
if let Err(e) = prune_snapshots(&snapshot_path) {
@@ -108,9 +103,9 @@ fn create_snapshot(snapshot_path: &PathBuf, db: &sled::Db) {
info!(%checksum);
let timestamp = SystemTime::now()
- .duration_since(UNIX_EPOCH)
- .unwrap()
- .as_secs();
+ .duration_since(UNIX_EPOCH)
+ .unwrap()
+ .as_secs();
// create a temporary directory for writing the snapshot
// we don't do this in the system tmpdir because it may
@@ -134,9 +129,9 @@ fn create_snapshot(snapshot_path: &PathBuf, db: &sled::Db) {
fn prune_snapshots(snapshot_path: &PathBuf) -> Result<(), AppError> {
let contents = fs::read_dir(snapshot_path)?;
let now = SystemTime::now()
- .duration_since(UNIX_EPOCH)
- .unwrap()
- .as_secs();
+ .duration_since(UNIX_EPOCH)
+ .unwrap()
+ .as_secs();
for name in contents {
let name = name?;
diff --git a/static/css/bulma-list.css b/apps/server/static/css/bulma-list.css
similarity index 100%
rename from static/css/bulma-list.css
rename to apps/server/static/css/bulma-list.css
diff --git a/static/css/bulma.min.css b/apps/server/static/css/bulma.min.css
similarity index 100%
rename from static/css/bulma.min.css
rename to apps/server/static/css/bulma.min.css
diff --git a/static/css/main.css b/apps/server/static/css/main.css
similarity index 100%
rename from static/css/main.css
rename to apps/server/static/css/main.css
diff --git a/static/favicon.svg b/apps/server/static/favicon.svg
similarity index 100%
rename from static/favicon.svg
rename to apps/server/static/favicon.svg
diff --git a/static/js/encoding-helper.js b/apps/server/static/js/encoding-helper.js
similarity index 100%
rename from static/js/encoding-helper.js
rename to apps/server/static/js/encoding-helper.js
diff --git a/static/js/encryption-helper.js b/apps/server/static/js/encryption-helper.js
similarity index 100%
rename from static/js/encryption-helper.js
rename to apps/server/static/js/encryption-helper.js
diff --git a/static/robots.txt b/apps/server/static/robots.txt
similarity index 100%
rename from static/robots.txt
rename to apps/server/static/robots.txt
diff --git a/templates/admin.html b/apps/server/templates/admin.html
similarity index 100%
rename from templates/admin.html
rename to apps/server/templates/admin.html
diff --git a/templates/admin_gallery.html b/apps/server/templates/admin_gallery.html
similarity index 100%
rename from templates/admin_gallery.html
rename to apps/server/templates/admin_gallery.html
diff --git a/templates/admin_view.html b/apps/server/templates/admin_view.html
similarity index 100%
rename from templates/admin_view.html
rename to apps/server/templates/admin_view.html
diff --git a/templates/atom.xml b/apps/server/templates/atom.xml
similarity index 100%
rename from templates/atom.xml
rename to apps/server/templates/atom.xml
diff --git a/templates/error.html b/apps/server/templates/error.html
similarity index 100%
rename from templates/error.html
rename to apps/server/templates/error.html
diff --git a/templates/feed.html b/apps/server/templates/feed.html
similarity index 100%
rename from templates/feed.html
rename to apps/server/templates/feed.html
diff --git a/templates/feed_add.html b/apps/server/templates/feed_add.html
similarity index 100%
rename from templates/feed_add.html
rename to apps/server/templates/feed_add.html
diff --git a/templates/feed_read.html b/apps/server/templates/feed_read.html
similarity index 100%
rename from templates/feed_read.html
rename to apps/server/templates/feed_read.html
diff --git a/templates/gallery.html b/apps/server/templates/gallery.html
similarity index 100%
rename from templates/gallery.html
rename to apps/server/templates/gallery.html
diff --git a/templates/icons/feeds.svg b/apps/server/templates/icons/feeds.svg
similarity index 100%
rename from templates/icons/feeds.svg
rename to apps/server/templates/icons/feeds.svg
diff --git a/templates/icons/lock.svg b/apps/server/templates/icons/lock.svg
similarity index 100%
rename from templates/icons/lock.svg
rename to apps/server/templates/icons/lock.svg
diff --git a/templates/icons/lock_square.svg b/apps/server/templates/icons/lock_square.svg
similarity index 100%
rename from templates/icons/lock_square.svg
rename to apps/server/templates/icons/lock_square.svg
diff --git a/templates/icons/mail.svg b/apps/server/templates/icons/mail.svg
similarity index 100%
rename from templates/icons/mail.svg
rename to apps/server/templates/icons/mail.svg
diff --git a/templates/icons/notification.svg b/apps/server/templates/icons/notification.svg
similarity index 100%
rename from templates/icons/notification.svg
rename to apps/server/templates/icons/notification.svg
diff --git a/templates/icons/rss.svg b/apps/server/templates/icons/rss.svg
similarity index 100%
rename from templates/icons/rss.svg
rename to apps/server/templates/icons/rss.svg
diff --git a/templates/icons/setting.svg b/apps/server/templates/icons/setting.svg
similarity index 100%
rename from templates/icons/setting.svg
rename to apps/server/templates/icons/setting.svg
diff --git a/templates/icons/signout.svg b/apps/server/templates/icons/signout.svg
similarity index 100%
rename from templates/icons/signout.svg
rename to apps/server/templates/icons/signout.svg
diff --git a/templates/icons/star.svg b/apps/server/templates/icons/star.svg
similarity index 100%
rename from templates/icons/star.svg
rename to apps/server/templates/icons/star.svg
diff --git a/templates/icons/user-plus.svg b/apps/server/templates/icons/user-plus.svg
similarity index 100%
rename from templates/icons/user-plus.svg
rename to apps/server/templates/icons/user-plus.svg
diff --git a/templates/icons/user-xmark.svg b/apps/server/templates/icons/user-xmark.svg
similarity index 100%
rename from templates/icons/user-xmark.svg
rename to apps/server/templates/icons/user-xmark.svg
diff --git a/templates/inbox.html b/apps/server/templates/inbox.html
similarity index 100%
rename from templates/inbox.html
rename to apps/server/templates/inbox.html
diff --git a/templates/inn.html b/apps/server/templates/inn.html
similarity index 100%
rename from templates/inn.html
rename to apps/server/templates/inn.html
diff --git a/templates/inn_create.html b/apps/server/templates/inn_create.html
similarity index 100%
rename from templates/inn_create.html
rename to apps/server/templates/inn_create.html
diff --git a/templates/inn_edit.html b/apps/server/templates/inn_edit.html
similarity index 100%
rename from templates/inn_edit.html
rename to apps/server/templates/inn_edit.html
diff --git a/templates/inn_list.html b/apps/server/templates/inn_list.html
similarity index 100%
rename from templates/inn_list.html
rename to apps/server/templates/inn_list.html
diff --git a/templates/key.html b/apps/server/templates/key.html
similarity index 100%
rename from templates/key.html
rename to apps/server/templates/key.html
diff --git a/templates/layout.html b/apps/server/templates/layout.html
similarity index 100%
rename from templates/layout.html
rename to apps/server/templates/layout.html
diff --git a/templates/message.html b/apps/server/templates/message.html
similarity index 100%
rename from templates/message.html
rename to apps/server/templates/message.html
diff --git a/templates/notification.html b/apps/server/templates/notification.html
similarity index 100%
rename from templates/notification.html
rename to apps/server/templates/notification.html
diff --git a/templates/post.html b/apps/server/templates/post.html
similarity index 100%
rename from templates/post.html
rename to apps/server/templates/post.html
diff --git a/templates/post_create.html b/apps/server/templates/post_create.html
similarity index 100%
rename from templates/post_create.html
rename to apps/server/templates/post_create.html
diff --git a/templates/post_edit.html b/apps/server/templates/post_edit.html
similarity index 100%
rename from templates/post_edit.html
rename to apps/server/templates/post_edit.html
diff --git a/templates/preview.html b/apps/server/templates/preview.html
similarity index 100%
rename from templates/preview.html
rename to apps/server/templates/preview.html
diff --git a/templates/reset.html b/apps/server/templates/reset.html
similarity index 100%
rename from templates/reset.html
rename to apps/server/templates/reset.html
diff --git a/templates/search.html b/apps/server/templates/search.html
similarity index 100%
rename from templates/search.html
rename to apps/server/templates/search.html
diff --git a/templates/show_recovery.html b/apps/server/templates/show_recovery.html
similarity index 100%
rename from templates/show_recovery.html
rename to apps/server/templates/show_recovery.html
diff --git a/templates/signin.html b/apps/server/templates/signin.html
similarity index 100%
rename from templates/signin.html
rename to apps/server/templates/signin.html
diff --git a/templates/signup.html b/apps/server/templates/signup.html
similarity index 100%
rename from templates/signup.html
rename to apps/server/templates/signup.html
diff --git a/templates/solo.html b/apps/server/templates/solo.html
similarity index 100%
rename from templates/solo.html
rename to apps/server/templates/solo.html
diff --git a/templates/solo_list.html b/apps/server/templates/solo_list.html
similarity index 100%
rename from templates/solo_list.html
rename to apps/server/templates/solo_list.html
diff --git a/templates/tag.html b/apps/server/templates/tag.html
similarity index 100%
rename from templates/tag.html
rename to apps/server/templates/tag.html
diff --git a/templates/upload.html b/apps/server/templates/upload.html
similarity index 100%
rename from templates/upload.html
rename to apps/server/templates/upload.html
diff --git a/templates/user.html b/apps/server/templates/user.html
similarity index 100%
rename from templates/user.html
rename to apps/server/templates/user.html
diff --git a/templates/user_list.html b/apps/server/templates/user_list.html
similarity index 100%
rename from templates/user_list.html
rename to apps/server/templates/user_list.html
diff --git a/templates/user_setting.html b/apps/server/templates/user_setting.html
similarity index 100%
rename from templates/user_setting.html
rename to apps/server/templates/user_setting.html
diff --git a/typos.toml b/apps/server/typos.toml
similarity index 100%
rename from typos.toml
rename to apps/server/typos.toml
diff --git a/bunfig.toml b/bunfig.toml
new file mode 100644
index 00000000..027d8652
--- /dev/null
+++ b/bunfig.toml
@@ -0,0 +1,2 @@
+[install.lockfile]
+print = "yarn" # required for CI/CLI tools that don't support bun.lockb files
diff --git a/package.json b/package.json
new file mode 100644
index 00000000..d35e32af
--- /dev/null
+++ b/package.json
@@ -0,0 +1,9 @@
+{
+ "engines": {
+ "node": ">=20"
+ },
+ "private": true,
+ "workspaces": [
+ "apps/client"
+ ]
+}
diff --git a/src/config.rs b/src/config.rs
deleted file mode 100644
index e4d9bd5a..00000000
--- a/src/config.rs
+++ /dev/null
@@ -1,72 +0,0 @@
-use serde::{Deserialize, Serialize};
-use std::fs::{self, read_to_string, File};
-use std::io::Write;
-use std::path::Path;
-use std::sync::LazyLock;
-use tracing::{info, warn};
-
-pub static CONFIG: LazyLock = LazyLock::new(Config::load_config);
-
-#[derive(Serialize, Deserialize)]
-pub struct Config {
- pub db: String,
- pub addr: String,
- pub rebuild_index: Option,
- pub(crate) avatars_path: String,
- pub(crate) inn_icons_path: String,
- pub(crate) upload_path: String,
- pub(crate) tantivy_path: String,
- pub(crate) proxy: String,
-}
-
-impl Config {
- fn load_config() -> Config {
- let cfg_file = std::env::args()
- .nth(1)
- .unwrap_or_else(|| "config.toml".to_owned());
- let config = if let Ok(config_toml_content) = read_to_string(cfg_file) {
- let config: Config = basic_toml::from_str(&config_toml_content).unwrap();
- config
- } else {
- warn!("Config file not found, using default config.toml");
- let config = Config::default();
- let toml = basic_toml::to_string(&config).unwrap();
- let mut cfg_file = File::create("config.toml").unwrap();
- cfg_file.write_all(toml.as_bytes()).unwrap();
- config
- };
-
- check_path(&config.avatars_path);
- check_path(&config.inn_icons_path);
- check_path(&config.upload_path);
- check_path(&config.tantivy_path);
-
- config
- }
-}
-
-impl Default for Config {
- fn default() -> Self {
- Config {
- db: "freedit.db".into(),
- addr: "127.0.0.1:3001".into(),
- rebuild_index: None,
- avatars_path: "static/imgs/avatars".into(),
- inn_icons_path: "static/imgs/inn_icons".into(),
- upload_path: "static/imgs/upload".into(),
- tantivy_path: "tantivy".into(),
- proxy: "".into(),
- }
- }
-}
-
-/// Create new dir if the path doesn't exist.
-fn check_path(path_str: &str) {
- let path = Path::new(path_str);
- if !path.exists() {
- fs::create_dir_all(path).unwrap();
- info!("create path: {}", path_str);
- } else {
- info!("{path_str} is ok");
- }
-}