mirror of
https://github.com/thegeneralist01/archivr
synced 2026-07-22 03:05:32 +02:00
Compare commits
No commits in common. "685b6cc7ea90cbf656360a81fb103d47b4b88a05" and "4311e85f95302dbde111595388a1816a82781f91" have entirely different histories.
685b6cc7ea
...
4311e85f95
12 changed files with 98 additions and 1043 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
|
@ -19,9 +19,6 @@
|
||||||
!ARCHIVR-MENTAL-MODEL.md
|
!ARCHIVR-MENTAL-MODEL.md
|
||||||
!NEXT.md
|
!NEXT.md
|
||||||
|
|
||||||
!modules/
|
|
||||||
!modules/**
|
|
||||||
|
|
||||||
!frontend/
|
!frontend/
|
||||||
!frontend/**
|
!frontend/**
|
||||||
frontend/node_modules/
|
frontend/node_modules/
|
||||||
|
|
|
||||||
48
Cargo.lock
generated
48
Cargo.lock
generated
|
|
@ -130,7 +130,6 @@ dependencies = [
|
||||||
"axum-extra",
|
"axum-extra",
|
||||||
"base64",
|
"base64",
|
||||||
"chrono",
|
"chrono",
|
||||||
"parking_lot",
|
|
||||||
"rand",
|
"rand",
|
||||||
"rusqlite",
|
"rusqlite",
|
||||||
"serde",
|
"serde",
|
||||||
|
|
@ -1010,15 +1009,6 @@ version = "0.8.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0"
|
checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "lock_api"
|
|
||||||
version = "0.4.14"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965"
|
|
||||||
dependencies = [
|
|
||||||
"scopeguard",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "log"
|
name = "log"
|
||||||
version = "0.4.28"
|
version = "0.4.28"
|
||||||
|
|
@ -1168,29 +1158,6 @@ dependencies = [
|
||||||
"vcpkg",
|
"vcpkg",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "parking_lot"
|
|
||||||
version = "0.12.5"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a"
|
|
||||||
dependencies = [
|
|
||||||
"lock_api",
|
|
||||||
"parking_lot_core",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "parking_lot_core"
|
|
||||||
version = "0.9.12"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1"
|
|
||||||
dependencies = [
|
|
||||||
"cfg-if",
|
|
||||||
"libc",
|
|
||||||
"redox_syscall",
|
|
||||||
"smallvec",
|
|
||||||
"windows-link",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "password-hash"
|
name = "password-hash"
|
||||||
version = "0.5.0"
|
version = "0.5.0"
|
||||||
|
|
@ -1298,15 +1265,6 @@ dependencies = [
|
||||||
"getrandom 0.2.17",
|
"getrandom 0.2.17",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "redox_syscall"
|
|
||||||
version = "0.5.18"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d"
|
|
||||||
dependencies = [
|
|
||||||
"bitflags",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "regex"
|
name = "regex"
|
||||||
version = "1.12.2"
|
version = "1.12.2"
|
||||||
|
|
@ -1473,12 +1431,6 @@ dependencies = [
|
||||||
"windows-sys 0.61.2",
|
"windows-sys 0.61.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "scopeguard"
|
|
||||||
version = "1.2.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "security-framework"
|
name = "security-framework"
|
||||||
version = "3.7.0"
|
version = "3.7.0"
|
||||||
|
|
|
||||||
|
|
@ -32,4 +32,3 @@ base64 = "0.22"
|
||||||
argon2 = { version = "0.5", features = ["std"] }
|
argon2 = { version = "0.5", features = ["std"] }
|
||||||
rand = { version = "0.8", features = ["std"] }
|
rand = { version = "0.8", features = ["std"] }
|
||||||
axum-extra = { version = "0.9", features = ["cookie"] }
|
axum-extra = { version = "0.9", features = ["cookie"] }
|
||||||
parking_lot = "0.12"
|
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,6 @@ chrono.workspace = true
|
||||||
base64.workspace = true
|
base64.workspace = true
|
||||||
serde_json.workspace = true
|
serde_json.workspace = true
|
||||||
rusqlite.workspace = true
|
rusqlite.workspace = true
|
||||||
parking_lot.workspace = true
|
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
tempfile.workspace = true
|
tempfile.workspace = true
|
||||||
|
|
|
||||||
|
|
@ -63,6 +63,6 @@ async fn main() -> Result<()> {
|
||||||
|
|
||||||
let listener = tokio::net::TcpListener::bind(addr).await?;
|
let listener = tokio::net::TcpListener::bind(addr).await?;
|
||||||
println!("archivr-server listening on http://{addr}");
|
println!("archivr-server listening on http://{addr}");
|
||||||
axum::serve(listener, app.into_make_service_with_connect_info::<SocketAddr>()).await?;
|
axum::serve(listener, app).await?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
File diff suppressed because one or more lines are too long
|
|
@ -4,7 +4,7 @@
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
<title>Archivr</title>
|
<title>Archivr</title>
|
||||||
<script type="module" crossorigin src="/assets/index-CbWD4KAy.js"></script>
|
<script type="module" crossorigin src="/assets/index-CWb0sQjJ.js"></script>
|
||||||
<link rel="stylesheet" crossorigin href="/assets/index-MrdP6h9x.css">
|
<link rel="stylesheet" crossorigin href="/assets/index-MrdP6h9x.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
|
||||||
|
|
@ -137,60 +137,6 @@ Auth and session handling will be designed when remote or public hosting becomes
|
||||||
- X/Twitter Tweet content scrape: [Tweet and Thread shorthands](#supported-shorthand-inputs). (These are saved as JSON files in `raw_tweets/`)
|
- X/Twitter Tweet content scrape: [Tweet and Thread shorthands](#supported-shorthand-inputs). (These are saved as JSON files in `raw_tweets/`)
|
||||||
- Instagram, Facebook, TikTok, Reddit, Snapchat: direct URLs or platform-prefixed shorthand passed through to `yt-dlp`
|
- Instagram, Facebook, TikTok, Reddit, Snapchat: direct URLs or platform-prefixed shorthand passed through to `yt-dlp`
|
||||||
|
|
||||||
### Hosting on NixOS
|
|
||||||
|
|
||||||
The flake exposes a `nixosModules.default` output. Add it to your system flake and
|
|
||||||
enable the service:
|
|
||||||
|
|
||||||
```nix
|
|
||||||
# flake.nix (your system flake)
|
|
||||||
{
|
|
||||||
inputs.archivr.url = "github:thegeneralist/archivr";
|
|
||||||
|
|
||||||
outputs = { nixpkgs, archivr, ... }: {
|
|
||||||
nixosConfigurations.myhost = nixpkgs.lib.nixosSystem {
|
|
||||||
modules = [
|
|
||||||
archivr.nixosModules.default
|
|
||||||
{
|
|
||||||
services.archivr-server = {
|
|
||||||
enable = true;
|
|
||||||
# listenAddress defaults to "127.0.0.1" (loopback only)
|
|
||||||
# port defaults to 8080
|
|
||||||
archives = [
|
|
||||||
{ id = "personal"; label = "Personal"; path = "/srv/archivr/personal/.archivr"; }
|
|
||||||
{ id = "work"; label = "Work"; path = "/srv/archivr/work/.archivr"; }
|
|
||||||
];
|
|
||||||
};
|
|
||||||
}
|
|
||||||
];
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
The module:
|
|
||||||
- Creates an `archivr` system user and group.
|
|
||||||
- Generates the TOML config from your options and stores the auth database under
|
|
||||||
`/var/lib/archivr-server/` (persists across upgrades).
|
|
||||||
- Runs under a hardened systemd unit (`ProtectSystem = strict`, `NoNewPrivileges`,
|
|
||||||
`PrivateTmp`, etc.). Archive directories are whitelisted for read-write access.
|
|
||||||
- Restarts automatically on failure.
|
|
||||||
|
|
||||||
**`openFirewall`** — set to `true` to open the TCP port derived from `bind`.
|
|
||||||
Only needed when binding to a non-loopback address:
|
|
||||||
|
|
||||||
```nix
|
|
||||||
services.archivr-server = {
|
|
||||||
listenAddress = "0.0.0.0";
|
|
||||||
port = 8080; # explicit, though 8080 is the default
|
|
||||||
openFirewall = true;
|
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|
||||||
**Archive directories** must be readable and writable by the `archivr` user.
|
|
||||||
Initialise them with `archivr init` first, then `chown -R archivr:archivr /srv/archivr`.
|
|
||||||
|
|
||||||
|
|
||||||
### Supported Shorthand Inputs
|
### Supported Shorthand Inputs
|
||||||
|
|
||||||
- YouTube video/short media:
|
- YouTube video/short media:
|
||||||
|
|
|
||||||
|
|
@ -16,12 +16,11 @@
|
||||||
inputs.nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
|
inputs.nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
|
||||||
|
|
||||||
outputs =
|
outputs =
|
||||||
{ nixpkgs, self, ... }:
|
{ nixpkgs, ... }:
|
||||||
let
|
let
|
||||||
lib = nixpkgs.lib;
|
lib = nixpkgs.lib;
|
||||||
systems = [
|
systems = [
|
||||||
"x86_64-linux"
|
"x86_64-linux"
|
||||||
"aarch64-linux"
|
|
||||||
"aarch64-darwin"
|
"aarch64-darwin"
|
||||||
];
|
];
|
||||||
in
|
in
|
||||||
|
|
@ -172,11 +171,6 @@
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
nixosModules = {
|
|
||||||
archivr-server = import ./modules/nixos/archivr-server.nix { inherit self; };
|
|
||||||
default = self.nixosModules.archivr-server;
|
|
||||||
};
|
|
||||||
|
|
||||||
devShells = lib.genAttrs systems (
|
devShells = lib.genAttrs systems (
|
||||||
system:
|
system:
|
||||||
let
|
let
|
||||||
|
|
|
||||||
|
|
@ -70,10 +70,8 @@ export default function App() {
|
||||||
}
|
}
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
// Load archives once authenticated (re-runs when authState changes so
|
// Mount: load archives, pick first, then parallel load
|
||||||
// it triggers correctly after first login or a session refresh).
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (authState !== 'authenticated') return
|
|
||||||
fetchArchives().then(list => {
|
fetchArchives().then(list => {
|
||||||
setArchives(list)
|
setArchives(list)
|
||||||
if (list.length > 0) {
|
if (list.length > 0) {
|
||||||
|
|
@ -81,7 +79,7 @@ export default function App() {
|
||||||
setArchiveId(first)
|
setArchiveId(first)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}, [authState])
|
}, [])
|
||||||
|
|
||||||
// Archive change: parallel load entries + runs + tags
|
// Archive change: parallel load entries + runs + tags
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
|
||||||
|
|
@ -1,190 +0,0 @@
|
||||||
# NixOS module for archivr-server.
|
|
||||||
# Consumed by flake.nix as:
|
|
||||||
# nixosModules.archivr-server = import ./modules/nixos/archivr-server.nix { inherit self; };
|
|
||||||
{ self }:
|
|
||||||
{ config, lib, pkgs, ... }:
|
|
||||||
let
|
|
||||||
cfg = config.services.archivr-server;
|
|
||||||
|
|
||||||
# Escape characters that would break TOML double-quoted strings.
|
|
||||||
escapeTOML = s: builtins.replaceStrings [ "\\" "\"" ] [ "\\\\" "\\\"" ] s;
|
|
||||||
|
|
||||||
# Derived bind string from separate address + port options.
|
|
||||||
# IPv6 addresses contain ":" and must be wrapped in brackets per RFC 2732.
|
|
||||||
bindStr =
|
|
||||||
let hasColon = builtins.match ".*:.*" cfg.listenAddress != null;
|
|
||||||
in if hasColon
|
|
||||||
then "[${cfg.listenAddress}]:${toString cfg.port}"
|
|
||||||
else "${cfg.listenAddress}:${toString cfg.port}";
|
|
||||||
|
|
||||||
# Generate the TOML registry file from NixOS options.
|
|
||||||
# The auth DB is pinned to the StateDirectory so it survives upgrades.
|
|
||||||
configFile = pkgs.writeText "archivr-server.toml" ''
|
|
||||||
bind = "${bindStr}"
|
|
||||||
auth_db_path = "/var/lib/archivr-server/archivr-auth.sqlite"
|
|
||||||
|
|
||||||
${lib.concatMapStrings (a: ''
|
|
||||||
[[archives]]
|
|
||||||
id = "${escapeTOML a.id}"
|
|
||||||
label = "${escapeTOML a.label}"
|
|
||||||
archive_path = "${escapeTOML a.path}"
|
|
||||||
|
|
||||||
'') cfg.archives}
|
|
||||||
'';
|
|
||||||
in
|
|
||||||
{
|
|
||||||
options.services.archivr-server = {
|
|
||||||
enable = lib.mkEnableOption "archivr web server";
|
|
||||||
|
|
||||||
package = lib.mkOption {
|
|
||||||
type = lib.types.package;
|
|
||||||
default = self.packages.${pkgs.system}.archivr-server;
|
|
||||||
defaultText = lib.literalExpression "archivr-server from the archivr flake";
|
|
||||||
description = "The archivr-server package to use.";
|
|
||||||
};
|
|
||||||
|
|
||||||
listenAddress = lib.mkOption {
|
|
||||||
type = lib.types.str;
|
|
||||||
default = "127.0.0.1";
|
|
||||||
example = "0.0.0.0";
|
|
||||||
description = ''
|
|
||||||
IP address to listen on. Defaults to loopback only (127.0.0.1).
|
|
||||||
Set to {code}`0.0.0.0` (or a specific interface address) to expose
|
|
||||||
the server on the network. Always put a reverse proxy with TLS
|
|
||||||
in front before doing this.
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
|
|
||||||
port = lib.mkOption {
|
|
||||||
type = lib.types.port;
|
|
||||||
default = 8080;
|
|
||||||
description = "TCP port to listen on.";
|
|
||||||
};
|
|
||||||
|
|
||||||
archives = lib.mkOption {
|
|
||||||
type = lib.types.listOf (lib.types.submodule {
|
|
||||||
options = {
|
|
||||||
id = lib.mkOption {
|
|
||||||
type = lib.types.str;
|
|
||||||
example = "personal";
|
|
||||||
description = "Unique archive identifier used in the config and API URLs.";
|
|
||||||
};
|
|
||||||
label = lib.mkOption {
|
|
||||||
type = lib.types.str;
|
|
||||||
example = "Personal";
|
|
||||||
description = "Display name shown in the UI archive switcher.";
|
|
||||||
};
|
|
||||||
path = lib.mkOption {
|
|
||||||
type = lib.types.str;
|
|
||||||
example = "/srv/archivr/personal/.archivr";
|
|
||||||
description = ''Absolute path to the .archivr directory created by {command}`archivr init`.
|
|
||||||
The parent directory (which also contains the {file}`store/` blob directory)
|
|
||||||
is whitelisted for read-write access under systemd hardening.'';
|
|
||||||
};
|
|
||||||
storePath = lib.mkOption {
|
|
||||||
type = lib.types.nullOr lib.types.str;
|
|
||||||
default = null;
|
|
||||||
description = ''
|
|
||||||
Absolute path to the store directory for this archive. Only needed
|
|
||||||
when the archive was initialised with a custom store path outside
|
|
||||||
the default sibling {file}`store/` location. When null (the default),
|
|
||||||
the parent of {option}`path` already covers the standard layout.
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
};
|
|
||||||
});
|
|
||||||
default = [];
|
|
||||||
description = ''
|
|
||||||
Archives to mount. Each entry maps to one {code}`[[archives]]` block in the
|
|
||||||
generated config file. The list must contain at least one entry.
|
|
||||||
'';
|
|
||||||
example = lib.literalExpression ''
|
|
||||||
[
|
|
||||||
{ id = "personal"; label = "Personal"; path = "/srv/archivr/personal/.archivr"; }
|
|
||||||
{ id = "work"; label = "Work"; path = "/srv/archivr/work/.archivr"; }
|
|
||||||
]
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
|
|
||||||
user = lib.mkOption {
|
|
||||||
type = lib.types.str;
|
|
||||||
default = "archivr";
|
|
||||||
description = "User account under which archivr-server runs.";
|
|
||||||
};
|
|
||||||
|
|
||||||
group = lib.mkOption {
|
|
||||||
type = lib.types.str;
|
|
||||||
default = "archivr";
|
|
||||||
description = "Group under which archivr-server runs.";
|
|
||||||
};
|
|
||||||
|
|
||||||
openFirewall = lib.mkOption {
|
|
||||||
type = lib.types.bool;
|
|
||||||
default = false;
|
|
||||||
description = ''
|
|
||||||
Open the firewall TCP port specified by
|
|
||||||
{option}`services.archivr-server.port`. Only meaningful when
|
|
||||||
{option}`services.archivr-server.listenAddress` is set to a
|
|
||||||
non-loopback address.
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
config = lib.mkIf cfg.enable {
|
|
||||||
assertions = [
|
|
||||||
{
|
|
||||||
assertion = cfg.archives != [];
|
|
||||||
message = "services.archivr-server.archives must contain at least one archive.";
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
users.users.${cfg.user} = {
|
|
||||||
isSystemUser = true;
|
|
||||||
group = cfg.group;
|
|
||||||
description = "archivr-server service user";
|
|
||||||
home = "/var/lib/archivr-server";
|
|
||||||
createHome = false; # StateDirectory creates it
|
|
||||||
};
|
|
||||||
users.groups.${cfg.group} = { };
|
|
||||||
|
|
||||||
systemd.services.archivr-server = {
|
|
||||||
description = "archivr web server";
|
|
||||||
wantedBy = [ "multi-user.target" ];
|
|
||||||
after = [ "network.target" ];
|
|
||||||
|
|
||||||
serviceConfig = {
|
|
||||||
ExecStart = "${cfg.package}/bin/archivr-server ${configFile}";
|
|
||||||
User = cfg.user;
|
|
||||||
Group = cfg.group;
|
|
||||||
|
|
||||||
# State directory — auth SQLite lives here across upgrades/restarts.
|
|
||||||
StateDirectory = "archivr-server";
|
|
||||||
StateDirectoryMode = "0750";
|
|
||||||
|
|
||||||
Restart = "on-failure";
|
|
||||||
RestartSec = "5s";
|
|
||||||
|
|
||||||
# Hardening — make the entire FS read-only except for the state
|
|
||||||
# directory and the parent directory of each mounted archive.
|
|
||||||
# Each archive_path is an .archivr dir; its sibling store/ dir (where
|
|
||||||
# capture artifacts are written) lives at the same level. Whitelisting
|
|
||||||
# the parent covers both without over-permissioning.
|
|
||||||
NoNewPrivileges = true;
|
|
||||||
PrivateTmp = true;
|
|
||||||
ProtectSystem = "strict";
|
|
||||||
ReadWritePaths =
|
|
||||||
[ "/var/lib/archivr-server" ]
|
|
||||||
++ (map (a: builtins.dirOf a.path) cfg.archives)
|
|
||||||
++ (lib.concatMap (a: lib.optional (a.storePath != null) a.storePath) cfg.archives);
|
|
||||||
RestrictAddressFamilies = [ "AF_INET" "AF_INET6" "AF_UNIX" ];
|
|
||||||
LockPersonality = true;
|
|
||||||
RestrictSUIDSGID = true;
|
|
||||||
RemoveIPC = true;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
networking.firewall = lib.mkIf cfg.openFirewall {
|
|
||||||
allowedTCPPorts = [ cfg.port ];
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue