Title
This commit is contained in:
parent
001a27c694
commit
fbf3d7640a
8 changed files with 110 additions and 38 deletions
24
FORMATS.md
Normal file
24
FORMATS.md
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
# Internal storage formats
|
||||
|
||||
This file describes the internal storage formats used for persistent data.
|
||||
|
||||
## Repository metadata
|
||||
|
||||
`meta.bin`
|
||||
|
||||
File structure:
|
||||
|
||||
* Version (1 byte): `0x00`
|
||||
* Time (8 bytes, big endian)
|
||||
* Title length (4 bytes, big endian) (may be zero)
|
||||
* Title
|
||||
* Repository URL length (4 bytes, big endian)
|
||||
* Repository URL
|
||||
* Commit hash length (4 bytes, big endian)
|
||||
* Commit hash
|
||||
* Content
|
||||
|
||||
Content is a concatenation of entries. Entry structure:
|
||||
* File path length (4 bytes, big endian)
|
||||
* File path
|
||||
* Hash (44 bytes)
|
||||
|
|
@ -211,6 +211,7 @@ pub async fn fetch_repo_tree_index_at_commit(
|
|||
repo_index,
|
||||
RepoMetadata {
|
||||
date: std::time::UNIX_EPOCH.elapsed().unwrap().as_secs(),
|
||||
title: String::new(),
|
||||
repo_url: repo_url.to_string(),
|
||||
commit_hash: commit_hash.to_string(),
|
||||
content: Vec::new(),
|
||||
|
|
@ -235,8 +236,8 @@ pub async fn fetch_repo_files(
|
|||
let repo_key: [u8; 32] = rand::rng().random();
|
||||
hasher.update(repo_key);
|
||||
let repo_id: [u8; 32] = hasher.finalize_reset().into();
|
||||
let mut repo_id_str = [0; 24];
|
||||
base64_turbo::URL_SAFE
|
||||
let mut repo_id_str = [0; 22];
|
||||
base64_turbo::URL_SAFE_NO_PAD
|
||||
.encode_into(&repo_id[0..16], &mut repo_id_str)
|
||||
.expect("unreachable");
|
||||
let repo_id_str = str::from_utf8(&repo_id_str).expect("unreachable");
|
||||
|
|
|
|||
11
src/cache.rs
11
src/cache.rs
|
|
@ -10,13 +10,22 @@ pub struct CacheEntry<T> {
|
|||
time: AtomicU64,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct Cache<K: Eq + Hash, V> {
|
||||
ttl: u64,
|
||||
size: usize,
|
||||
content: DashMap<K, CacheEntry<V>>,
|
||||
}
|
||||
|
||||
impl<K: Eq + Hash, V> Default for Cache<K, V> {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
ttl: 0,
|
||||
size: 0,
|
||||
content: DashMap::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<K: Clone + Eq + Hash, V: Clone> Cache<K, V> {
|
||||
pub fn sweep(&self) {
|
||||
let now = std::time::UNIX_EPOCH.elapsed().unwrap().as_secs();
|
||||
|
|
|
|||
19
src/repo.rs
19
src/repo.rs
|
|
@ -11,9 +11,10 @@ use std::{
|
|||
const REPO_METADATA_FILE_NAME: &str = "meta.bin";
|
||||
const VERSION: [u8; 1] = [0];
|
||||
|
||||
#[derive(Debug)]
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct RepoMetadata {
|
||||
pub date: u64,
|
||||
pub title: String,
|
||||
pub repo_url: String,
|
||||
pub commit_hash: String,
|
||||
pub content: Vec<u8>,
|
||||
|
|
@ -75,6 +76,10 @@ impl RepoMetadata {
|
|||
let now = std::time::UNIX_EPOCH.elapsed().unwrap().as_secs();
|
||||
file.write_all(&now.to_be_bytes())?;
|
||||
|
||||
file.write_all(&(self.title.len() as u32).to_be_bytes())?;
|
||||
|
||||
file.write_all(self.title.as_bytes())?;
|
||||
|
||||
file.write_all(&(self.repo_url.len() as u32).to_be_bytes())?;
|
||||
|
||||
file.write_all(self.repo_url.as_bytes())?;
|
||||
|
|
@ -104,6 +109,17 @@ impl RepoMetadata {
|
|||
file.read_exact(&mut date)?;
|
||||
let date = u64::from_be_bytes(date);
|
||||
|
||||
let mut title_len = [0u8; 4];
|
||||
file.read_exact(&mut title_len)?;
|
||||
let title_len = u32::from_be_bytes(title_len) as usize;
|
||||
if title_len > MAX_URL_SIZE {
|
||||
return Err(ReadRepoMetadataError::InvalidFormat);
|
||||
}
|
||||
|
||||
let mut title = vec![0; title_len];
|
||||
file.read_exact(&mut title)?;
|
||||
let title = String::from_utf8(title)?;
|
||||
|
||||
let mut repo_url_len = [0u8; 4];
|
||||
file.read_exact(&mut repo_url_len)?;
|
||||
let repo_url_len = u32::from_be_bytes(repo_url_len) as usize;
|
||||
|
|
@ -131,6 +147,7 @@ impl RepoMetadata {
|
|||
|
||||
Ok(Self {
|
||||
date,
|
||||
title,
|
||||
repo_url,
|
||||
commit_hash,
|
||||
content,
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
use crate::{cache, config::Config, repo::ReadRepoMetadataError, templates};
|
||||
use crate::{cache, config::Config, repo, templates};
|
||||
|
||||
use askama::Template;
|
||||
use log::error;
|
||||
|
|
@ -16,7 +16,7 @@ pub fn make_router(config: &'static Config) -> impl Handler {
|
|||
|
||||
let metadata_cache: &'static _ = Box::leak(Box::new(cache::Cache::<
|
||||
String,
|
||||
templates::Directory,
|
||||
(repo::RepoMetadata, templates::Directory),
|
||||
>::default()));
|
||||
let file_cache: &'static _ =
|
||||
Box::leak(Box::new(cache::Cache::<(String, String), String>::default()));
|
||||
|
|
@ -26,7 +26,6 @@ pub fn make_router(config: &'static Config) -> impl Handler {
|
|||
|
||||
(
|
||||
trillium_caching_headers::CachingHeaders::new(),
|
||||
//trillium_static_compiled::static_compiled!("./static").with_index_file("index.html"),
|
||||
Router::new()
|
||||
.get("/", |conn: Conn| async move {
|
||||
conn.ok(crate::templates::Home {}.render().unwrap())
|
||||
|
|
@ -34,10 +33,14 @@ pub fn make_router(config: &'static Config) -> impl Handler {
|
|||
.post("/fetch", move |mut conn: Conn| async move {
|
||||
if let Ok(request_body) = conn.request_body().with_max_len(8192).read_bytes().await
|
||||
{
|
||||
let mut title = String::new();
|
||||
let mut repo_url = None;
|
||||
let mut commit_hash = None;
|
||||
for (key, val) in form_urlencoded::parse(&request_body) {
|
||||
match key.as_ref() {
|
||||
"title" => {
|
||||
title = val.to_string();
|
||||
}
|
||||
"repo-url" => {
|
||||
repo_url = Some(val);
|
||||
}
|
||||
|
|
@ -61,6 +64,7 @@ pub fn make_router(config: &'static Config) -> impl Handler {
|
|||
)
|
||||
.await
|
||||
.expect("todo handle error");
|
||||
repo_metadata.title = title;
|
||||
crate::api_client::fetch_repo_files(
|
||||
config,
|
||||
&mut client,
|
||||
|
|
@ -71,7 +75,6 @@ pub fn make_router(config: &'static Config) -> impl Handler {
|
|||
.await
|
||||
.expect("todo handle error");
|
||||
}
|
||||
//let planet = conn.param("planet").unwrap();
|
||||
conn.ok(crate::templates::Home {}.render().unwrap())
|
||||
})
|
||||
.get("/r/:hash/*", move |conn: Conn| {
|
||||
|
|
@ -81,21 +84,14 @@ pub fn make_router(config: &'static Config) -> impl Handler {
|
|||
};
|
||||
|
||||
let fetch_metadata = |key| {
|
||||
//let mut repo_hash = [0; 24];
|
||||
//if dbg!(base64_turbo::URL_SAFE.decode_into(key, &mut repo_hash))
|
||||
// != Ok(16)
|
||||
//{
|
||||
// return None;
|
||||
//}
|
||||
//let repo_hash = &repo_hash[0..16];
|
||||
let repo_dir = PathBuf::from(&config.data_dir)
|
||||
.join(crate::SUBDIR_REPOS)
|
||||
.join(key);
|
||||
let repo_metadata =
|
||||
let mut repo_metadata =
|
||||
match crate::repo::RepoMetadata::read_from_file(config, &repo_dir) {
|
||||
Ok(v) => v,
|
||||
Err(e) => {
|
||||
if let ReadRepoMetadataError::CannotOpenFile(e) = &e {
|
||||
if let repo::ReadRepoMetadataError::CannotOpenFile(e) = &e {
|
||||
if e.kind() == ErrorKind::NotFound {
|
||||
return None;
|
||||
}
|
||||
|
|
@ -119,6 +115,10 @@ pub fn make_router(config: &'static Config) -> impl Handler {
|
|||
name: name.into(),
|
||||
path: dbg!(entry.file_path).into(),
|
||||
hash: entry.hash.into(),
|
||||
link: format!(
|
||||
"/r/{repo_hash_str}/{0}",
|
||||
entry.file_path
|
||||
),
|
||||
}),
|
||||
path,
|
||||
);
|
||||
|
|
@ -128,9 +128,11 @@ pub fn make_router(config: &'static Config) -> impl Handler {
|
|||
}
|
||||
}
|
||||
}
|
||||
Some(root)
|
||||
// Save memory
|
||||
repo_metadata.content.clear();
|
||||
Some((repo_metadata, root))
|
||||
};
|
||||
let Some(root) =
|
||||
let Some((repo_metadata, root)) =
|
||||
metadata_cache.fetch(repo_hash_str.to_string(), fetch_metadata)
|
||||
else {
|
||||
return conn.with_status(404);
|
||||
|
|
@ -180,14 +182,16 @@ pub fn make_router(config: &'static Config) -> impl Handler {
|
|||
file_lang,
|
||||
giallo::ThemeVariant::Single("catppuccin-frappe"),
|
||||
);
|
||||
let highlighted = hl_registry.highlight(&file_content, &hl_options).unwrap_or_else(|_e| {
|
||||
// If extension is unknown
|
||||
let hl_options = giallo::HighlightOptions::new(
|
||||
giallo::PLAIN_GRAMMAR_NAME,
|
||||
giallo::ThemeVariant::Single("catppuccin-frappe"),
|
||||
);
|
||||
hl_registry.highlight(&file_content, &hl_options).unwrap()
|
||||
});
|
||||
let highlighted = hl_registry
|
||||
.highlight(&file_content, &hl_options)
|
||||
.unwrap_or_else(|_e| {
|
||||
// If extension is unknown
|
||||
let hl_options = giallo::HighlightOptions::new(
|
||||
giallo::PLAIN_GRAMMAR_NAME,
|
||||
giallo::ThemeVariant::Single("catppuccin-frappe"),
|
||||
);
|
||||
hl_registry.highlight(&file_content, &hl_options).unwrap()
|
||||
});
|
||||
let html = giallo::HtmlRenderer::default().render(
|
||||
&highlighted,
|
||||
&giallo::RenderOptions {
|
||||
|
|
@ -195,12 +199,13 @@ pub fn make_router(config: &'static Config) -> impl Handler {
|
|||
..Default::default()
|
||||
},
|
||||
);
|
||||
conn.ok(crate::templates::Repo {
|
||||
let template = crate::templates::Repo {
|
||||
content: html.clone(),
|
||||
root,
|
||||
}
|
||||
.render()
|
||||
.unwrap())
|
||||
path: conn.path().split('/').map(|s| s.to_string()).collect(),
|
||||
title: repo_metadata.title,
|
||||
};
|
||||
conn.ok(template.render().unwrap())
|
||||
}
|
||||
})
|
||||
.get("/e/:secret", |conn: Conn| async move {
|
||||
|
|
|
|||
|
|
@ -7,11 +7,18 @@ use trillium_askama::Template;
|
|||
#[template(path = "home.html")]
|
||||
pub struct Home {}
|
||||
|
||||
/// View of a page in a repository
|
||||
#[derive(Template)]
|
||||
#[template(path = "repo.html")]
|
||||
pub struct Repo {
|
||||
/// HTML-rendered content of the file
|
||||
pub content: String,
|
||||
/// Repository tree from the root
|
||||
pub root: Directory,
|
||||
/// Current file path
|
||||
pub path: Vec<String>,
|
||||
/// Repository title
|
||||
pub title: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Template)]
|
||||
|
|
@ -30,7 +37,7 @@ pub struct Repo {
|
|||
</details>
|
||||
</li>
|
||||
{% when Entry::File(file) %}
|
||||
<li class="tree-file"><span class="filename">{{ file.name|escape }}</span></li>
|
||||
<li class="tree-file"><span class="filename"><a href="{{ file.link|urlencode }}">{{ file.name|escape }}</a></span></li>
|
||||
{% endmatch %}
|
||||
"#,
|
||||
ext = "html",
|
||||
|
|
@ -46,6 +53,7 @@ pub struct File {
|
|||
pub name: String,
|
||||
pub path: String,
|
||||
pub hash: String,
|
||||
pub link: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Default)]
|
||||
|
|
|
|||
|
|
@ -8,12 +8,18 @@
|
|||
<p>Import any repository from any Forgejo instance using its main URL. The full commit hash is also needed, so the right version is fetched.</p>
|
||||
<p>Token is optional. It is needed if the repository is private. In that case, create an API token with read access to the repository. It is revocable at any moment and only grants the selected permissions.</p>
|
||||
<form action="fetch" method="post">
|
||||
<label for="f-title">Repo title:</label>
|
||||
<input type="text" id="f-title" name="title" aria-describedby="f-title-help"/>
|
||||
<span id="f-title-help">Title that will be displayed for this anonymous repository (can be modified later).</span><br/>
|
||||
<label for="f-repo-url">Repo URL:</label>
|
||||
<input type="text" id="f-repo-url" name="repo-url" required/><br/>
|
||||
<input type="text" id="f-repo-url" name="repo-url" aria-describedby="f-repo-title-help" required/>
|
||||
<span id="f-repo-title-help">URL of the original repository.</span><br/><br/>
|
||||
<label for="f-commit">Commit hash:</label>
|
||||
<input type="text" id="f-commit" name="commit" required/><br/>
|
||||
<input type="text" id="f-commit" name="commit" aria-describedby="f-repo-title-help" required/>
|
||||
<span id="f-repo-title-help">Full commit hash at which the repository is fetched.</span><br/><br/>
|
||||
<label for="f-token">Token:</label>
|
||||
<input type="text" id="f-token" name="token"/><br/>
|
||||
<input type="text" id="f-token" name="token" aria-describedby="f-repo-title-help"/>
|
||||
<span id="f-repo-title-help">Access token, in case the repository is private.</span><br/><br/>
|
||||
<input type="submit" value="Fetch"/>
|
||||
</form>
|
||||
</body>
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ html, body {
|
|||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<h1>Titre</h1>
|
||||
<h1>Blindforge</h1>
|
||||
</header>
|
||||
<div id="tree">
|
||||
<ul class="tree">
|
||||
|
|
@ -35,8 +35,10 @@ html, body {
|
|||
</ul>
|
||||
</div>
|
||||
<div id="page">
|
||||
<h2>Fichier</h2>
|
||||
<span><a>Root</a> / <a>Dossier</a> / Fichier</span>
|
||||
<h2>{{ title }}</h2>
|
||||
<span>
|
||||
{% for element in path %}/{{ element }}{% endfor %}
|
||||
</span>
|
||||
<div id="content">
|
||||
{{ content|safe }}
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Reference in a new issue