Compare commits

...

2 commits

Author SHA1 Message Date
0352989083 refactor: use sequential version ids per website
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2023-03-04 18:49:47 +01:00
f9c82e5601 feat: add version_files api endpoint 2023-03-04 17:40:58 +01:00
14 changed files with 1849 additions and 5593 deletions

View file

@ -2,7 +2,7 @@
**static site management system**
![CI status](https://ci.thetadev.de/api/badges/ThetaDev/Talon/status.svg)
[![CI status](https://ci.thetadev.de/api/badges/ThetaDev/Talon/status.svg)](https://ci.thetadev.de/ThetaDev/Talon)
---

View file

@ -211,58 +211,66 @@ impl TalonApi {
}
/// Get version
#[oai(path = "/website/:subdomain/version/:id", method = "get")]
#[oai(path = "/website/:subdomain/version/:version", method = "get")]
async fn version_get(
&self,
talon: Data<&Talon>,
subdomain: Path<String>,
id: Path<u32>,
version: Path<u32>,
) -> Result<Json<Version>> {
talon
.db
.get_version(&subdomain, *id)
.map(|v| Json(Version::from((*id, v))))
.get_version(&subdomain, *version)
.map(|v| Json(Version::from((*version, v))))
.map_err(Error::from)
}
/// Get version files
#[oai(path = "/website/:subdomain/version/:version/files", method = "get")]
async fn version_files(
&self,
talon: Data<&Talon>,
subdomain: Path<String>,
version: Path<u32>,
) -> Result<Json<Vec<String>>> {
talon.db.version_exists(&subdomain, *version)?;
talon
.db
.get_version_files(&subdomain, *version)
.map(|r| r.map(|f| f.0))
.collect::<Result<Vec<_>, _>>()
.map(Json)
.map_err(Error::from)
}
/// Delete version
#[oai(path = "/website/:subdomain/version/:id", method = "delete")]
#[oai(path = "/website/:subdomain/version/:version", method = "delete")]
async fn version_delete(
&self,
auth: ApiKeyAuthorization,
talon: Data<&Talon>,
subdomain: Path<String>,
id: Path<u32>,
version: Path<u32>,
) -> Result<()> {
auth.check_subdomain(&subdomain, Access::Modify)?;
talon.db.delete_version(&subdomain, *id, true)?;
talon.db.delete_version(&subdomain, *version, true)?;
Ok(())
}
/// Insert a new version into the database
fn insert_version(
talon: &Talon,
subdomain: &str,
id: u32,
fallback: Option<String>,
spa: bool,
mut version_data: BTreeMap<String, String>,
) -> Result<()> {
) -> Result<u32> {
version_data.remove("fallback");
version_data.remove("spa");
// Validata fallback path
if let Some(fallback) = &fallback {
if let Err(e) = talon.storage.get_file(id, fallback, &Default::default()) {
// Remove the uploaded files of the bad version
let _ = talon.db.delete_version(subdomain, id, false);
return Err(ApiError::InvalidFallback(e.to_string()).into());
}
}
talon.db.insert_version(
let id = talon.db.insert_version(
subdomain,
id,
&db::model::Version {
data: version_data,
fallback,
@ -270,10 +278,33 @@ impl TalonApi {
..Default::default()
},
)?;
Ok(id)
}
/// Set the given version as the most recent one
fn finalize_version(
talon: &Talon,
subdomain: &str,
version: u32,
fallback: Option<&str>,
) -> Result<()> {
// Validata fallback path
if let Some(fallback) = fallback {
if let Err(e) =
talon
.storage
.get_file(subdomain, version, fallback, &Default::default())
{
// Remove the bad version
let _ = talon.db.delete_version(subdomain, version, false);
return Err(ApiError::InvalidFallback(e.to_string()).into());
}
}
talon.db.update_website(
subdomain,
db::model::WebsiteUpdate {
latest_version: Some(Some(id)),
latest_version: Some(Some(version)),
..Default::default()
},
)?;
@ -303,11 +334,12 @@ impl TalonApi {
data: Binary<Vec<u8>>,
) -> Result<()> {
auth.check_subdomain(&subdomain, Access::Upload)?;
let vid = talon.db.new_version_id()?;
let version =
Self::insert_version(&talon, &subdomain, fallback.clone(), spa.0, version_data.0)?;
talon
.storage
.insert_zip_archive(Cursor::new(data.as_slice()), vid)?;
Self::insert_version(&talon, &subdomain, vid, fallback.0, spa.0, version_data.0)
.insert_zip_archive(Cursor::new(data.as_slice()), &subdomain, version)?;
Self::finalize_version(&talon, &subdomain, version, fallback.as_deref())
}
/// Upload a new version (.tar.gz archive)
@ -333,8 +365,11 @@ impl TalonApi {
data: Binary<Vec<u8>>,
) -> Result<()> {
auth.check_subdomain(&subdomain, Access::Upload)?;
let vid = talon.db.new_version_id()?;
talon.storage.insert_tgz_archive(data.as_slice(), vid)?;
Self::insert_version(&talon, &subdomain, vid, fallback.0, spa.0, version_data.0)
let version =
Self::insert_version(&talon, &subdomain, fallback.clone(), spa.0, version_data.0)?;
talon
.storage
.insert_tgz_archive(data.as_slice(), &subdomain, version)?;
Self::finalize_version(&talon, &subdomain, version, fallback.as_deref())
}
}

View file

@ -259,14 +259,6 @@ impl Db {
String::from_utf8(key).map_err(|e| DbError::Other(format!("could not parse key: {e}")))
}
fn split_key(key: Vec<u8>) -> Result<(String, String)> {
let key_str = Self::key_to_string(key)?;
key_str
.split_once(':')
.map(|(id, p)| (id.to_owned(), p.to_owned()))
.ok_or_else(|| DbError::Other(format!("invalid key: {key_str}")))
}
fn split_version_key(key: Vec<u8>) -> Result<(String, u32)> {
let key_str = Self::key_to_string(key)?;
key_str
@ -275,47 +267,68 @@ impl Db {
.ok_or_else(|| DbError::Other(format!("invalid key: {key_str}")))
}
fn increment(old: Option<&[u8]>) -> Option<Vec<u8>> {
let number = match old {
Some(bytes) => {
let array: [u8; 4] = bytes.try_into().unwrap();
let number = u32::from_be_bytes(array);
number + 1
}
None => 1,
};
Some(number.to_be_bytes().to_vec())
fn split_file_key(key: Vec<u8>) -> Result<(String, String)> {
let key_str = Self::key_to_string(key)?;
let mut parts = key_str.split(':');
parts.next(); // Skip subdomain part
match (parts.next(), parts.next()) {
(Some(id), Some(p)) => Ok((id.to_owned(), p.to_owned())),
_ => Err(DbError::Other(format!("invalid key: {key_str}"))),
}
}
/// Returns an error if the website does not exist
pub fn version_exists(&self, subdomain: &str, id: u32) -> Result<()> {
let key = Self::version_key(subdomain, id);
if !self.i.versions.contains_key(&key)? {
Err(DbError::NotExists("version", key))
} else {
Ok(())
}
}
/// Get a version from the database
pub fn get_version(&self, subdomain: &str, id: u32) -> Result<Version> {
let data = self.i.versions.get(Self::version_key(subdomain, id))?;
data.and_then(|data| rmp_serde::from_slice::<Version>(data.as_ref()).ok())
.ok_or_else(|| DbError::NotExists("version", subdomain.to_owned()))
}
let key = Self::version_key(subdomain, id);
/// Get a new unique version id
pub fn new_version_id(&self) -> Result<u32> {
Ok(u32::from_be_bytes(
self.i
.db
.update_and_fetch("vid_count", Self::increment)?
.unwrap()
.as_ref()
.try_into()
.unwrap(),
))
let data = self.i.versions.get(&key)?;
data.and_then(|data| rmp_serde::from_slice::<Version>(data.as_ref()).ok())
.ok_or_else(|| DbError::NotExists("version", key))
}
/// Insert a new version into the database
pub fn insert_version(&self, subdomain: &str, id: u32, version: &Version) -> Result<()> {
///
/// Returns the ID of the new version
pub fn insert_version(&self, subdomain: &str, version: &Version) -> Result<u32> {
let ws = self
.i
.websites
.update_and_fetch(subdomain, |data| match data {
Some(data) => match rmp_serde::from_slice::<Website>(data) {
Ok(mut w) => {
w.vid_count += 1;
rmp_serde::to_vec(&w).ok()
}
Err(_) => None,
},
None => todo!(),
})?
.and_then(|data| rmp_serde::from_slice::<Website>(&data).ok());
let id = match ws {
Some(ws) => ws.vid_count,
None => return Err(DbError::NotExists("website", subdomain.to_owned())),
};
let key = Self::version_key(subdomain, id);
let data = rmp_serde::to_vec(version)?;
self.i
.versions
.compare_and_swap(&key, None::<&[u8]>, Some(data))?
.map_err(|_| DbError::Exists("version", key))?;
Ok(())
Ok(id)
}
/// internal method for deleting a version from the database
@ -323,10 +336,10 @@ impl Db {
/// this method does not lock the db or update the associated website
fn _delete_version(&self, subdomain: &str, id: u32, should_exist: bool) -> Result<()> {
// Remove all files associated with the version
for f in self.get_version_files(id) {
for f in self.get_version_files(subdomain, id) {
match f {
Ok((path, _)) => {
self.delete_file(id, &path, false)?;
self.delete_file(subdomain, id, &path, false)?;
}
Err(DbError::Sled(e)) => return Err(DbError::Sled(e)),
Err(_) => {}
@ -397,19 +410,24 @@ impl Db {
})
}
fn file_key(version: u32, path: &str) -> String {
format!("{version}:{path}")
fn file_key(subdomain: &str, version: u32, path: &str) -> String {
format!("{subdomain}:{version}:{path}")
}
/// Get the hash of a file in the database
pub fn get_file_opt(&self, version: u32, path: &str) -> Result<Option<Vec<u8>>> {
let key = Self::file_key(version, path);
pub fn get_file_opt(
&self,
subdomain: &str,
version: u32,
path: &str,
) -> Result<Option<Vec<u8>>> {
let key = Self::file_key(subdomain, version, path);
Ok(self.i.files.get(key)?.map(|hash| hash.to_vec()))
}
/// Get the hash of a file in the database
pub fn get_file(&self, version: u32, path: &str) -> Result<Vec<u8>> {
let key = Self::file_key(version, path);
pub fn get_file(&self, subdomain: &str, version: u32, path: &str) -> Result<Vec<u8>> {
let key = Self::file_key(subdomain, version, path);
match self.i.files.get(&key)? {
Some(hash) => Ok(hash.to_vec()),
None => Err(DbError::NotExists("file", key)),
@ -417,8 +435,14 @@ impl Db {
}
/// Insert a file into the database
pub fn insert_file(&self, version: u32, path: &str, hash: &[u8]) -> Result<()> {
let key = Self::file_key(version, path);
pub fn insert_file(
&self,
subdomain: &str,
version: u32,
path: &str,
hash: &[u8],
) -> Result<()> {
let key = Self::file_key(subdomain, version, path);
self.i
.files
.compare_and_swap(&key, None::<&[u8]>, Some(hash))?
@ -426,8 +450,14 @@ impl Db {
}
/// Delete a file in the database
pub fn delete_file(&self, version: u32, path: &str, should_exist: bool) -> Result<()> {
let key = Self::file_key(version, path);
pub fn delete_file(
&self,
subdomain: &str,
version: u32,
path: &str,
should_exist: bool,
) -> Result<()> {
let key = Self::file_key(subdomain, version, path);
let res = self.i.files.remove(&key)?;
if should_exist && res.is_none() {
@ -442,13 +472,14 @@ impl Db {
/// Result: Tuples of file path and hash
pub fn get_version_files(
&self,
id: u32,
subdomain: &str,
version: u32,
) -> impl DoubleEndedIterator<Item = Result<(String, Vec<u8>)>> {
let key = Self::file_key(id, "");
let key = Self::file_key(subdomain, version, "");
self.i.files.scan_prefix(key).map(|r| {
r.map_err(DbError::from).and_then(|(k, v)| {
let (_, path) = Self::split_key(k.to_vec())?;
let (_, path) = Self::split_file_key(k.to_vec())?;
Ok((path, v.to_vec()))
})
})

View file

@ -22,6 +22,10 @@ pub struct Website {
pub source_url: Option<String>,
/// Icon for the source link
pub source_icon: Option<SourceIcon>,
/// Version ID counter
///
/// value + 1 will be the next version ID
pub vid_count: u32,
}
impl Default for Website {
@ -34,6 +38,7 @@ impl Default for Website {
visibility: Default::default(),
source_url: Default::default(),
source_icon: Default::default(),
vid_count: Default::default(),
}
}
}

View file

@ -36,26 +36,34 @@ pub async fn page(request: &Request, talon: Data<&Talon>) -> Result<Response> {
let ws = talon.db.get_website(subdomain)?;
let vid = ws.latest_version.ok_or(PageError::NoVersion)?;
let (file, ok) = match talon
.storage
.get_file(vid, request.uri().path(), request.headers())
{
Ok(file) => (file, true),
Err(StorageError::NotFound(f)) => {
let version = talon.db.get_version(subdomain, vid)?;
if let Some(fallback) = &version.fallback {
(
talon.storage.get_file(vid, fallback, request.headers())?,
version.spa,
)
} else if version.spa {
(talon.storage.get_file(vid, "", request.headers())?, true)
} else {
return Err(StorageError::NotFound(f).into());
let (file, ok) =
match talon
.storage
.get_file(subdomain, vid, request.uri().path(), request.headers())
{
Ok(file) => (file, true),
Err(StorageError::NotFound(f)) => {
let version = talon.db.get_version(subdomain, vid)?;
if let Some(fallback) = &version.fallback {
(
talon
.storage
.get_file(subdomain, vid, fallback, request.headers())?,
version.spa,
)
} else if version.spa {
(
talon
.storage
.get_file(subdomain, vid, "", request.headers())?,
true,
)
} else {
return Err(StorageError::NotFound(f).into());
}
}
}
Err(e) => return Err(e.into()),
};
Err(e) => return Err(e.into()),
};
Ok(match file.rd_path {
Some(rd_path) => Redirect::moved_permanent(rd_path).into_response(),

View file

@ -114,6 +114,7 @@ impl Storage {
pub fn insert_file<P: AsRef<Path>>(
&self,
file_path: P,
subdomain: &str,
version: u32,
site_path: &str,
) -> Result<()> {
@ -153,7 +154,7 @@ impl Storage {
}
}
self.db.insert_file(version, site_path, &hash)?;
self.db.insert_file(subdomain, version, site_path, &hash)?;
Ok(())
}
@ -210,30 +211,40 @@ impl Storage {
}
/// Insert a directory of files into the store
pub fn insert_dir<P: AsRef<Path>>(&self, dir: P, version: u32) -> Result<()> {
pub fn insert_dir<P: AsRef<Path>>(&self, dir: P, subdomain: &str, version: u32) -> Result<()> {
Self::visit_files(dir, "", &|file_path, site_path| {
self.insert_file(file_path, version, site_path)
self.insert_file(file_path, subdomain, version, site_path)
})?;
Ok(())
}
/// Insert the contents of a zip archive into the store
pub fn insert_zip_archive(&self, reader: impl Read + Seek, version: u32) -> Result<()> {
pub fn insert_zip_archive(
&self,
reader: impl Read + Seek,
subdomain: &str,
version: u32,
) -> Result<()> {
let temp = TempDir::with_prefix(TMPDIR_PREFIX)?;
let mut zip = ZipArchive::new(reader)?;
zip.extract(temp.path())?;
let import_path = Self::fix_archive_path(temp.path())?;
self.insert_dir(import_path, version)
self.insert_dir(import_path, subdomain, version)
}
/// Insert the contents of a tar.gz archive into the store
pub fn insert_tgz_archive(&self, reader: impl Read, version: u32) -> Result<()> {
pub fn insert_tgz_archive(
&self,
reader: impl Read,
subdomain: &str,
version: u32,
) -> Result<()> {
let temp = TempDir::with_prefix(TMPDIR_PREFIX)?;
let decoder = GzDecoder::new(reader);
let mut archive = tar::Archive::new(decoder);
archive.unpack(temp.path())?;
let import_path = Self::fix_archive_path(temp.path())?;
self.insert_dir(import_path, version)
self.insert_dir(import_path, subdomain, version)
}
/// Get the path of a file with the given hash while creating the subdirectory
@ -302,7 +313,13 @@ impl Storage {
/// Get a file using the raw site path and the website version
///
/// HTTP headers are used to determine if the compressed version of a file should be returned.
pub fn get_file(&self, version: u32, site_path: &str, headers: &HeaderMap) -> Result<GotFile> {
pub fn get_file(
&self,
subdomain: &str,
version: u32,
site_path: &str,
headers: &HeaderMap,
) -> Result<GotFile> {
let sp = util::trim_site_path(site_path);
let mut new_path: Cow<str> = sp.into();
let mut rd_path = None;
@ -315,7 +332,7 @@ impl Storage {
// Attempt to access the following pages
// 1. Site path directly
// 2. Site path + `/index.html`
match self.db.get_file_opt(version, sp)? {
match self.db.get_file_opt(subdomain, version, sp)? {
Some(h) => {
hash = Some(h);
}
@ -334,7 +351,7 @@ impl Storage {
Some(hash) => hash,
None => self
.db
.get_file_opt(version, &new_path)?
.get_file_opt(subdomain, version, &new_path)?
.ok_or_else(|| StorageError::NotFound(sp.to_owned()))?,
};

166
tests/fixtures/mod.rs vendored
View file

@ -19,12 +19,6 @@ pub const SUBDOMAIN_2: &str = "spotify-gender-ex";
pub const SUBDOMAIN_3: &str = "rustypipe";
pub const SUBDOMAIN_4: &str = "spa";
pub const VERSION_1_1: u32 = 1;
pub const VERSION_1_2: u32 = 2;
pub const VERSION_2_1: u32 = 3;
pub const VERSION_3_1: u32 = 4;
pub const VERSION_4_1: u32 = 5;
pub const HASH_1_1_INDEX: [u8; 32] =
hex!("3b5f6bad5376897435def176d0fe77e5b9b4f0deafc7491fc27262650744ad68");
pub const HASH_1_1_STYLE: [u8; 32] =
@ -76,7 +70,7 @@ fn insert_websites(db: &Db) {
&Website {
name: "ThetaDev".to_owned(),
created_at: datetime!(2023-02-18 16:30 +0),
latest_version: Some(VERSION_1_2),
latest_version: Some(2),
color: Some(2068974),
visibility: talon::model::Visibility::Featured,
..Default::default()
@ -88,7 +82,7 @@ fn insert_websites(db: &Db) {
&Website {
name: "Spotify-Gender-Ex".to_owned(),
created_at: datetime!(2023-02-18 16:30 +0),
latest_version: Some(VERSION_2_1),
latest_version: Some(1),
color: Some(1947988),
visibility: talon::model::Visibility::Featured,
source_url: Some("https://github.com/Theta-Dev/Spotify-Gender-Ex".to_owned()),
@ -102,7 +96,7 @@ fn insert_websites(db: &Db) {
&Website {
name: "RustyPipe".to_owned(),
created_at: datetime!(2023-02-20 18:30 +0),
latest_version: Some(VERSION_3_1),
latest_version: Some(1),
color: Some(7943647),
visibility: talon::model::Visibility::Featured,
source_url: Some("https://code.thetadev.de/ThetaDev/rustypipe".to_owned()),
@ -116,7 +110,7 @@ fn insert_websites(db: &Db) {
&Website {
name: "SvelteKit SPA".to_owned(),
created_at: datetime!(2023-03-03 22:00 +0),
latest_version: Some(VERSION_4_1),
latest_version: Some(1),
color: Some(16727552),
visibility: talon::model::Visibility::Hidden,
..Default::default()
@ -130,17 +124,18 @@ fn insert_websites(db: &Db) {
"Deployed by".to_owned(),
"https://github.com/Theta-Dev/Talon/actions/runs/1352014628".to_owned(),
);
assert_eq!(db.new_version_id().unwrap(), VERSION_1_1);
db.insert_version(
SUBDOMAIN_1,
VERSION_1_1,
&Version {
created_at: datetime!(2023-02-18 16:30 +0),
data: v1_data,
..Default::default()
},
)
.unwrap();
assert_eq!(
db.insert_version(
SUBDOMAIN_1,
&Version {
created_at: datetime!(2023-02-18 16:30 +0),
data: v1_data,
..Default::default()
},
)
.unwrap(),
1
);
let mut v2_data = BTreeMap::new();
v2_data.insert("Version".to_owned(), "v0.1.1".to_owned());
@ -148,51 +143,54 @@ fn insert_websites(db: &Db) {
"Deployed by".to_owned(),
"https://github.com/Theta-Dev/Talon/actions/runs/1354755231".to_owned(),
);
assert_eq!(db.new_version_id().unwrap(), VERSION_1_2);
db.insert_version(
SUBDOMAIN_1,
VERSION_1_2,
&Version {
created_at: datetime!(2023-02-18 16:52 +0),
data: v2_data,
..Default::default()
},
)
.unwrap();
assert_eq!(
db.insert_version(
SUBDOMAIN_1,
&Version {
created_at: datetime!(2023-02-18 16:52 +0),
data: v2_data,
..Default::default()
},
)
.unwrap(),
2
);
assert_eq!(db.new_version_id().unwrap(), VERSION_2_1);
db.insert_version(
SUBDOMAIN_2,
VERSION_2_1,
&Version {
created_at: datetime!(2023-02-18 16:30 +0),
..Default::default()
},
)
.unwrap();
assert_eq!(db.new_version_id().unwrap(), VERSION_3_1);
db.insert_version(
SUBDOMAIN_3,
VERSION_3_1,
&Version {
created_at: datetime!(2023-02-20 18:30 +0),
..Default::default()
},
)
.unwrap();
assert_eq!(db.new_version_id().unwrap(), VERSION_4_1);
db.insert_version(
SUBDOMAIN_4,
VERSION_4_1,
&Version {
created_at: datetime!(2023-03-03 22:00 +0),
fallback: Some("200.html".to_owned()),
spa: true,
..Default::default()
},
)
.unwrap();
assert_eq!(
db.insert_version(
SUBDOMAIN_2,
&Version {
created_at: datetime!(2023-02-18 16:30 +0),
..Default::default()
},
)
.unwrap(),
1
);
assert_eq!(
db.insert_version(
SUBDOMAIN_3,
&Version {
created_at: datetime!(2023-02-20 18:30 +0),
..Default::default()
},
)
.unwrap(),
1
);
assert_eq!(
db.insert_version(
SUBDOMAIN_4,
&Version {
created_at: datetime!(2023-03-03 22:00 +0),
fallback: Some("200.html".to_owned()),
spa: true,
..Default::default()
},
)
.unwrap(),
1
);
}
#[fixture]
@ -201,44 +199,47 @@ pub fn db() -> DbTest {
let db = Db::new(&temp).unwrap();
insert_websites(&db);
db.insert_file(VERSION_1_1, "index.html", &HASH_1_1_INDEX)
db.insert_file(SUBDOMAIN_1, 1, "index.html", &HASH_1_1_INDEX)
.unwrap();
db.insert_file(VERSION_1_1, "style.css", &HASH_1_1_STYLE)
db.insert_file(SUBDOMAIN_1, 1, "style.css", &HASH_1_1_STYLE)
.unwrap();
db.insert_file(VERSION_1_2, "index.html", &HASH_1_2_INDEX)
db.insert_file(SUBDOMAIN_1, 2, "index.html", &HASH_1_2_INDEX)
.unwrap();
db.insert_file(VERSION_1_2, "assets/style.css", &HASH_1_2_STYLE)
db.insert_file(SUBDOMAIN_1, 2, "assets/style.css", &HASH_1_2_STYLE)
.unwrap();
db.insert_file(
VERSION_1_2,
SUBDOMAIN_1,
2,
"assets/image.jpg",
&hex!("901d291a47a8a9b55c06f84e5e5f82fd2dcee65cac1406d6e878b805d45c1e93"),
)
.unwrap();
db.insert_file(
VERSION_1_2,
SUBDOMAIN_1,
2,
"assets/test.js",
&hex!("b6ed35f5ae339a35a8babb11a91ff90c1a62ef250d30fa98e59500e8dbb896fa"),
)
.unwrap();
db.insert_file(
VERSION_1_2,
SUBDOMAIN_1,
2,
"data/example.txt",
&hex!("bae6bdae8097c24f9a99028e04bfc8d5e0a0c318955316db0e7b955def9c1dbb"),
)
.unwrap();
db.insert_file(VERSION_2_1, "index.html", &HASH_2_1_INDEX)
db.insert_file(SUBDOMAIN_2, 1, "index.html", &HASH_2_1_INDEX)
.unwrap();
db.insert_file(VERSION_2_1, "gex_style.css", &HASH_2_1_STYLE)
db.insert_file(SUBDOMAIN_2, 1, "gex_style.css", &HASH_2_1_STYLE)
.unwrap();
db.insert_file(VERSION_3_1, "index.html", &HASH_3_1_INDEX)
db.insert_file(SUBDOMAIN_3, 1, "index.html", &HASH_3_1_INDEX)
.unwrap();
db.insert_file(VERSION_3_1, "rp_style.css", &HASH_3_1_STYLE)
db.insert_file(SUBDOMAIN_3, 1, "rp_style.css", &HASH_3_1_STYLE)
.unwrap();
db.insert_file(VERSION_3_1, "page2/index.html", &HASH_3_1_PAGE2)
db.insert_file(SUBDOMAIN_3, 1, "page2/index.html", &HASH_3_1_PAGE2)
.unwrap();
DbTest { db, _temp: temp }
@ -266,25 +267,26 @@ pub fn tln() -> TalonTest {
talon
.storage
.insert_dir(path!("tests" / "testfiles" / "ThetaDev0"), VERSION_1_1)
.insert_dir(path!("tests" / "testfiles" / "ThetaDev0"), SUBDOMAIN_1, 1)
.unwrap();
talon
.storage
.insert_dir(path!("tests" / "testfiles" / "ThetaDev1"), VERSION_1_2)
.insert_dir(path!("tests" / "testfiles" / "ThetaDev1"), SUBDOMAIN_1, 2)
.unwrap();
talon
.storage
.insert_dir(path!("tests" / "testfiles" / "GenderEx"), VERSION_2_1)
.insert_dir(path!("tests" / "testfiles" / "GenderEx"), SUBDOMAIN_2, 1)
.unwrap();
talon
.storage
.insert_dir(path!("tests" / "testfiles" / "RustyPipe"), VERSION_3_1)
.insert_dir(path!("tests" / "testfiles" / "RustyPipe"), SUBDOMAIN_3, 1)
.unwrap();
talon
.storage
.insert_tgz_archive(
File::open(path!("tests" / "testfiles" / "archive" / "spa.tar.gz")).unwrap(),
VERSION_4_1,
SUBDOMAIN_4,
1,
)
.unwrap();

View file

@ -2,14 +2,14 @@
source: tests/tests.rs
expression: data
---
{"type":"website","key":"rustypipe","value":{"name":"RustyPipe","created_at":[2023,51,18,30,0,0,0,0,0],"latest_version":4,"color":7943647,"visibility":"featured","source_url":"https://code.thetadev.de/ThetaDev/rustypipe","source_icon":"gitea"}}
{"type":"website","key":"spa","value":{"name":"SvelteKit SPA","created_at":[2023,62,22,0,0,0,0,0,0],"latest_version":5,"color":16727552,"visibility":"hidden","source_url":null,"source_icon":null}}
{"type":"website","key":"spotify-gender-ex","value":{"name":"Spotify-Gender-Ex","created_at":[2023,49,16,30,0,0,0,0,0],"latest_version":3,"color":1947988,"visibility":"featured","source_url":"https://github.com/Theta-Dev/Spotify-Gender-Ex","source_icon":"github"}}
{"type":"version","key":"rustypipe:4","value":{"created_at":[2023,51,18,30,0,0,0,0,0],"data":{},"fallback":null,"spa":false}}
{"type":"version","key":"spa:5","value":{"created_at":[2023,62,22,0,0,0,0,0,0],"data":{},"fallback":"200.html","spa":true}}
{"type":"version","key":"spotify-gender-ex:3","value":{"created_at":[2023,49,16,30,0,0,0,0,0],"data":{},"fallback":null,"spa":false}}
{"type":"file","key":"3:gex_style.css","value":"fc825b409a49724af8f5b3c4ad15e175e68095ea746237a7b46152d3f383f541"}
{"type":"file","key":"3:index.html","value":"6c5d37546616519e8973be51515b8a90898b4675f7b6d01f2d891edb686408a2"}
{"type":"file","key":"4:index.html","value":"cc31423924cf1f124750825861ab1ccc675e755921fc2fa111c0a98e8c346a5e"}
{"type":"file","key":"4:page2/index.html","value":"be4f409ca0adcb21cdc7130cde63031718406726f889ef97ac8870c90b330a75"}
{"type":"file","key":"4:rp_style.css","value":"ee4fc4911a56e627c047a29ba3085131939d8d487759b9149d42aaab89ce8993"}
{"type":"website","key":"rustypipe","value":{"name":"RustyPipe","created_at":[2023,51,18,30,0,0,0,0,0],"latest_version":1,"color":7943647,"visibility":"featured","source_url":"https://code.thetadev.de/ThetaDev/rustypipe","source_icon":"gitea","vid_count":1}}
{"type":"website","key":"spa","value":{"name":"SvelteKit SPA","created_at":[2023,62,22,0,0,0,0,0,0],"latest_version":1,"color":16727552,"visibility":"hidden","source_url":null,"source_icon":null,"vid_count":1}}
{"type":"website","key":"spotify-gender-ex","value":{"name":"Spotify-Gender-Ex","created_at":[2023,49,16,30,0,0,0,0,0],"latest_version":1,"color":1947988,"visibility":"featured","source_url":"https://github.com/Theta-Dev/Spotify-Gender-Ex","source_icon":"github","vid_count":1}}
{"type":"version","key":"rustypipe:1","value":{"created_at":[2023,51,18,30,0,0,0,0,0],"data":{},"fallback":null,"spa":false}}
{"type":"version","key":"spa:1","value":{"created_at":[2023,62,22,0,0,0,0,0,0],"data":{},"fallback":"200.html","spa":true}}
{"type":"version","key":"spotify-gender-ex:1","value":{"created_at":[2023,49,16,30,0,0,0,0,0],"data":{},"fallback":null,"spa":false}}
{"type":"file","key":"rustypipe:1:index.html","value":"cc31423924cf1f124750825861ab1ccc675e755921fc2fa111c0a98e8c346a5e"}
{"type":"file","key":"rustypipe:1:page2/index.html","value":"be4f409ca0adcb21cdc7130cde63031718406726f889ef97ac8870c90b330a75"}
{"type":"file","key":"rustypipe:1:rp_style.css","value":"ee4fc4911a56e627c047a29ba3085131939d8d487759b9149d42aaab89ce8993"}
{"type":"file","key":"spotify-gender-ex:1:gex_style.css","value":"fc825b409a49724af8f5b3c4ad15e175e68095ea746237a7b46152d3f383f541"}
{"type":"file","key":"spotify-gender-ex:1:index.html","value":"6c5d37546616519e8973be51515b8a90898b4675f7b6d01f2d891edb686408a2"}

View file

@ -2,24 +2,24 @@
source: tests/tests.rs
expression: data
---
{"type":"website","key":"-","value":{"name":"ThetaDev","created_at":[2023,49,16,30,0,0,0,0,0],"latest_version":2,"color":2068974,"visibility":"featured","source_url":null,"source_icon":null}}
{"type":"website","key":"rustypipe","value":{"name":"RustyPipe","created_at":[2023,51,18,30,0,0,0,0,0],"latest_version":4,"color":7943647,"visibility":"featured","source_url":"https://code.thetadev.de/ThetaDev/rustypipe","source_icon":"gitea"}}
{"type":"website","key":"spa","value":{"name":"SvelteKit SPA","created_at":[2023,62,22,0,0,0,0,0,0],"latest_version":5,"color":16727552,"visibility":"hidden","source_url":null,"source_icon":null}}
{"type":"website","key":"spotify-gender-ex","value":{"name":"Spotify-Gender-Ex","created_at":[2023,49,16,30,0,0,0,0,0],"latest_version":3,"color":1947988,"visibility":"featured","source_url":"https://github.com/Theta-Dev/Spotify-Gender-Ex","source_icon":"github"}}
{"type":"website","key":"-","value":{"name":"ThetaDev","created_at":[2023,49,16,30,0,0,0,0,0],"latest_version":2,"color":2068974,"visibility":"featured","source_url":null,"source_icon":null,"vid_count":2}}
{"type":"website","key":"rustypipe","value":{"name":"RustyPipe","created_at":[2023,51,18,30,0,0,0,0,0],"latest_version":1,"color":7943647,"visibility":"featured","source_url":"https://code.thetadev.de/ThetaDev/rustypipe","source_icon":"gitea","vid_count":1}}
{"type":"website","key":"spa","value":{"name":"SvelteKit SPA","created_at":[2023,62,22,0,0,0,0,0,0],"latest_version":1,"color":16727552,"visibility":"hidden","source_url":null,"source_icon":null,"vid_count":1}}
{"type":"website","key":"spotify-gender-ex","value":{"name":"Spotify-Gender-Ex","created_at":[2023,49,16,30,0,0,0,0,0],"latest_version":1,"color":1947988,"visibility":"featured","source_url":"https://github.com/Theta-Dev/Spotify-Gender-Ex","source_icon":"github","vid_count":1}}
{"type":"version","key":"-:1","value":{"created_at":[2023,49,16,30,0,0,0,0,0],"data":{"Deployed by":"https://github.com/Theta-Dev/Talon/actions/runs/1352014628","Version":"v0.1.0"},"fallback":null,"spa":false}}
{"type":"version","key":"-:2","value":{"created_at":[2023,49,16,52,0,0,0,0,0],"data":{"Deployed by":"https://github.com/Theta-Dev/Talon/actions/runs/1354755231","Version":"v0.1.1"},"fallback":null,"spa":false}}
{"type":"version","key":"rustypipe:4","value":{"created_at":[2023,51,18,30,0,0,0,0,0],"data":{},"fallback":null,"spa":false}}
{"type":"version","key":"spa:5","value":{"created_at":[2023,62,22,0,0,0,0,0,0],"data":{},"fallback":"200.html","spa":true}}
{"type":"version","key":"spotify-gender-ex:3","value":{"created_at":[2023,49,16,30,0,0,0,0,0],"data":{},"fallback":null,"spa":false}}
{"type":"file","key":"1:index.html","value":"3b5f6bad5376897435def176d0fe77e5b9b4f0deafc7491fc27262650744ad68"}
{"type":"file","key":"1:style.css","value":"356f131c825fbf604797c7e9c85352549d81db8af91fee834016d075110af026"}
{"type":"file","key":"2:assets/image.jpg","value":"901d291a47a8a9b55c06f84e5e5f82fd2dcee65cac1406d6e878b805d45c1e93"}
{"type":"file","key":"2:assets/style.css","value":"356f131c825fbf604797c7e9c85352549d81db8af91fee834016d075110af026"}
{"type":"file","key":"2:assets/test.js","value":"b6ed35f5ae339a35a8babb11a91ff90c1a62ef250d30fa98e59500e8dbb896fa"}
{"type":"file","key":"2:data/example.txt","value":"bae6bdae8097c24f9a99028e04bfc8d5e0a0c318955316db0e7b955def9c1dbb"}
{"type":"file","key":"2:index.html","value":"a44816e6c3b650bdf88e6532659ba07ef187c2113ae311da9709e056aec8eadb"}
{"type":"file","key":"3:gex_style.css","value":"fc825b409a49724af8f5b3c4ad15e175e68095ea746237a7b46152d3f383f541"}
{"type":"file","key":"3:index.html","value":"6c5d37546616519e8973be51515b8a90898b4675f7b6d01f2d891edb686408a2"}
{"type":"file","key":"4:index.html","value":"cc31423924cf1f124750825861ab1ccc675e755921fc2fa111c0a98e8c346a5e"}
{"type":"file","key":"4:page2/index.html","value":"be4f409ca0adcb21cdc7130cde63031718406726f889ef97ac8870c90b330a75"}
{"type":"file","key":"4:rp_style.css","value":"ee4fc4911a56e627c047a29ba3085131939d8d487759b9149d42aaab89ce8993"}
{"type":"version","key":"rustypipe:1","value":{"created_at":[2023,51,18,30,0,0,0,0,0],"data":{},"fallback":null,"spa":false}}
{"type":"version","key":"spa:1","value":{"created_at":[2023,62,22,0,0,0,0,0,0],"data":{},"fallback":"200.html","spa":true}}
{"type":"version","key":"spotify-gender-ex:1","value":{"created_at":[2023,49,16,30,0,0,0,0,0],"data":{},"fallback":null,"spa":false}}
{"type":"file","key":"-:1:index.html","value":"3b5f6bad5376897435def176d0fe77e5b9b4f0deafc7491fc27262650744ad68"}
{"type":"file","key":"-:1:style.css","value":"356f131c825fbf604797c7e9c85352549d81db8af91fee834016d075110af026"}
{"type":"file","key":"-:2:assets/image.jpg","value":"901d291a47a8a9b55c06f84e5e5f82fd2dcee65cac1406d6e878b805d45c1e93"}
{"type":"file","key":"-:2:assets/style.css","value":"356f131c825fbf604797c7e9c85352549d81db8af91fee834016d075110af026"}
{"type":"file","key":"-:2:assets/test.js","value":"b6ed35f5ae339a35a8babb11a91ff90c1a62ef250d30fa98e59500e8dbb896fa"}
{"type":"file","key":"-:2:data/example.txt","value":"bae6bdae8097c24f9a99028e04bfc8d5e0a0c318955316db0e7b955def9c1dbb"}
{"type":"file","key":"-:2:index.html","value":"a44816e6c3b650bdf88e6532659ba07ef187c2113ae311da9709e056aec8eadb"}
{"type":"file","key":"rustypipe:1:index.html","value":"cc31423924cf1f124750825861ab1ccc675e755921fc2fa111c0a98e8c346a5e"}
{"type":"file","key":"rustypipe:1:page2/index.html","value":"be4f409ca0adcb21cdc7130cde63031718406726f889ef97ac8870c90b330a75"}
{"type":"file","key":"rustypipe:1:rp_style.css","value":"ee4fc4911a56e627c047a29ba3085131939d8d487759b9149d42aaab89ce8993"}
{"type":"file","key":"spotify-gender-ex:1:gex_style.css","value":"fc825b409a49724af8f5b3c4ad15e175e68095ea746237a7b46152d3f383f541"}
{"type":"file","key":"spotify-gender-ex:1:index.html","value":"6c5d37546616519e8973be51515b8a90898b4675f7b6d01f2d891edb686408a2"}

View file

@ -11,23 +11,26 @@ expression: "vec![ws1, ws2, ws3]"
visibility: featured,
source_url: None,
source_icon: None,
vid_count: 2,
),
Website(
name: "Spotify-Gender-Ex",
created_at: (2023, 49, 16, 30, 0, 0, 0, 0, 0),
latest_version: Some(3),
latest_version: Some(1),
color: Some(1947988),
visibility: featured,
source_url: Some("https://github.com/Theta-Dev/Spotify-Gender-Ex"),
source_icon: Some(github),
vid_count: 1,
),
Website(
name: "RustyPipe",
created_at: (2023, 51, 18, 30, 0, 0, 0, 0, 0),
latest_version: Some(4),
latest_version: Some(1),
color: Some(7943647),
visibility: featured,
source_url: Some("https://code.thetadev.de/ThetaDev/rustypipe"),
source_icon: Some(gitea),
vid_count: 1,
),
]

View file

@ -11,32 +11,36 @@ expression: websites
visibility: featured,
source_url: None,
source_icon: None,
vid_count: 2,
)),
("rustypipe", Website(
name: "RustyPipe",
created_at: (2023, 51, 18, 30, 0, 0, 0, 0, 0),
latest_version: Some(4),
latest_version: Some(1),
color: Some(7943647),
visibility: featured,
source_url: Some("https://code.thetadev.de/ThetaDev/rustypipe"),
source_icon: Some(gitea),
vid_count: 1,
)),
("spa", Website(
name: "SvelteKit SPA",
created_at: (2023, 62, 22, 0, 0, 0, 0, 0, 0),
latest_version: Some(5),
latest_version: Some(1),
color: Some(16727552),
visibility: hidden,
source_url: None,
source_icon: None,
vid_count: 1,
)),
("spotify-gender-ex", Website(
name: "Spotify-Gender-Ex",
created_at: (2023, 49, 16, 30, 0, 0, 0, 0, 0),
latest_version: Some(3),
latest_version: Some(1),
color: Some(1947988),
visibility: featured,
source_url: Some("https://github.com/Theta-Dev/Spotify-Gender-Ex"),
source_icon: Some(github),
vid_count: 1,
)),
]

View file

@ -10,4 +10,5 @@ Website(
visibility: hidden,
source_url: Some("https://example.com"),
source_icon: Some(link),
vid_count: 2,
)

View file

@ -94,30 +94,29 @@ mod database {
#[rstest]
fn get_version(db: DbTest) {
let version = db.get_version(SUBDOMAIN_1, VERSION_1_1).unwrap();
let version = db.get_version(SUBDOMAIN_1, 1).unwrap();
insta::assert_ron_snapshot!(version);
}
#[rstest]
fn delete_version(db: DbTest) {
db.delete_version(SUBDOMAIN_1, VERSION_1_2, true).unwrap();
db.delete_version(SUBDOMAIN_1, 2, true).unwrap();
assert!(matches!(
db.get_version(SUBDOMAIN_1, VERSION_1_2).unwrap_err(),
db.get_version(SUBDOMAIN_1, 2).unwrap_err(),
DbError::NotExists(_, _)
));
assert!(matches!(
db.delete_version(SUBDOMAIN_1, VERSION_1_2, true)
.unwrap_err(),
db.delete_version(SUBDOMAIN_1, 2, true).unwrap_err(),
DbError::NotExists(_, _)
));
db.delete_version(SUBDOMAIN_1, VERSION_1_2, false).unwrap();
db.delete_version(SUBDOMAIN_1, 2, false).unwrap();
// Check if files were deleted
assert!(db.get_version_files(VERSION_1_2).next().is_none());
assert!(db.get_version_files(SUBDOMAIN_1, 2).next().is_none());
// Check if latest version was updated
let ws = db.get_website(SUBDOMAIN_1).unwrap();
assert_eq!(ws.latest_version, Some(VERSION_1_1));
assert_eq!(ws.latest_version, Some(1));
}
#[rstest]
@ -135,33 +134,34 @@ mod database {
.get_website_version_ids(SUBDOMAIN_1)
.map(|v| v.unwrap())
.collect::<Vec<_>>();
assert_eq!(ids, vec![VERSION_1_1, VERSION_1_2]);
assert_eq!(ids, vec![1, 2]);
}
#[rstest]
fn get_file(db: DbTest) {
let hash = db.get_file(VERSION_1_1, "index.html").unwrap();
let hash = db.get_file(SUBDOMAIN_1, 1, "index.html").unwrap();
assert_eq!(hash, HASH_1_1_INDEX);
}
#[rstest]
fn delete_file(db: DbTest) {
db.delete_file(VERSION_1_1, "index.html", true).unwrap();
db.delete_file(SUBDOMAIN_1, 1, "index.html", true).unwrap();
assert!(matches!(
db.get_file(VERSION_1_1, "index.html").unwrap_err(),
db.get_file(SUBDOMAIN_1, 1, "index.html").unwrap_err(),
DbError::NotExists(_, _)
));
assert!(matches!(
db.delete_file(VERSION_1_1, "index.html", true).unwrap_err(),
db.delete_file(SUBDOMAIN_1, 1, "index.html", true)
.unwrap_err(),
DbError::NotExists(_, _)
));
db.delete_file(VERSION_1_1, "index.html", false).unwrap();
db.delete_file(SUBDOMAIN_1, 1, "index.html", false).unwrap();
}
#[rstest]
fn get_version_files(db: DbTest) {
let files = db
.get_version_files(VERSION_1_1)
.get_version_files(SUBDOMAIN_1, 1)
.map(|f| f.unwrap())
.collect::<Vec<_>>();
assert_eq!(
@ -201,10 +201,10 @@ mod storage {
let temp = temp_testdir::TempDir::default();
let store = Storage::new(temp.to_path_buf(), db_empty.clone(), Default::default());
store.insert_dir(dir, 1).unwrap();
store.insert_dir(dir, SUBDOMAIN_1, 1).unwrap();
let files = db_empty
.get_version_files(1)
.get_version_files(SUBDOMAIN_1, 1)
.map(|f| f.unwrap())
.collect::<Vec<_>>();
insta::assert_ron_snapshot!("insert_files", files);
@ -223,11 +223,11 @@ mod storage {
let store = Storage::new(temp.to_path_buf(), db_empty.clone(), Default::default());
store
.insert_zip_archive(File::open(archive).unwrap(), 1)
.insert_zip_archive(File::open(archive).unwrap(), SUBDOMAIN_1, 1)
.unwrap();
let files = db_empty
.get_version_files(1)
.get_version_files(SUBDOMAIN_1, 1)
.map(|f| f.unwrap())
.collect::<Vec<_>>();
insta::assert_ron_snapshot!("insert_files", files);
@ -246,11 +246,11 @@ mod storage {
let store = Storage::new(temp.to_path_buf(), db_empty.clone(), Default::default());
store
.insert_tgz_archive(File::open(archive).unwrap(), 1)
.insert_tgz_archive(File::open(archive).unwrap(), SUBDOMAIN_1, 1)
.unwrap();
let files = db_empty
.get_version_files(1)
.get_version_files(SUBDOMAIN_1, 1)
.map(|f| f.unwrap())
.collect::<Vec<_>>();
insta::assert_ron_snapshot!("insert_files", files);
@ -278,9 +278,9 @@ mod storage {
});
let store = Storage::new(temp.to_path_buf(), db_empty.clone(), cfg);
store.insert_dir(dir, 1).unwrap();
store.insert_dir(dir, SUBDOMAIN_1, 1).unwrap();
for f in db_empty.get_version_files(1) {
for f in db_empty.get_version_files(SUBDOMAIN_1, 1) {
let hash = f.unwrap().1;
let hash_str = hash.encode_hex::<String>();
let path = temp.join(&hash_str[..2]).join(&hash_str);
@ -297,15 +297,16 @@ mod storage {
}
#[rstest]
#[case::index("br", VERSION_1_2, "", false, "text/html", None)]
#[case::nocmp("", VERSION_1_2, "assets/style.css", true, "text/css", None)]
#[case::gzip("gzip", VERSION_1_2, "assets/style.css", true, "text/css", None)]
#[case::br("br", VERSION_1_2, "assets/style.css", true, "text/css", None)]
#[case::image("br", VERSION_1_2, "assets/image.jpg", false, "image/jpeg", None)]
#[case::subdir("br", VERSION_3_1, "page2", false, "text/html", Some("/page2/"))]
#[case::index("br", SUBDOMAIN_1, 2, "", false, "text/html", None)]
#[case::nocmp("", SUBDOMAIN_1, 2, "assets/style.css", true, "text/css", None)]
#[case::gzip("gzip", SUBDOMAIN_1, 2, "assets/style.css", true, "text/css", None)]
#[case::br("br", SUBDOMAIN_1, 2, "assets/style.css", true, "text/css", None)]
#[case::image("br", SUBDOMAIN_1, 2, "assets/image.jpg", false, "image/jpeg", None)]
#[case::subdir("br", SUBDOMAIN_3, 1, "page2", false, "text/html", Some("/page2/"))]
fn get_file(
tln: TalonTest,
#[case] encoding: &str,
#[case] subdomain: &str,
#[case] version: u32,
#[case] path: &str,
#[case] compressible: bool,
@ -325,7 +326,10 @@ mod storage {
None
};
let index_file = tln.storage.get_file(version, path, &headers).unwrap();
let index_file = tln
.storage
.get_file(subdomain, version, path, &headers)
.unwrap();
dbg!(&index_file);
assert!(index_file.file_path.is_file());
assert_eq!(
@ -620,38 +624,43 @@ mod page {
#[case] ok: bool,
#[case] hash: &[u8],
) {
let vid = tln.db.new_version_id().unwrap();
const SUBDOMAIN: &str = "fallback";
tln.db
.insert_website(
"fallback",
SUBDOMAIN,
&talon::db::model::Website {
latest_version: Some(vid),
..Default::default()
},
)
.unwrap();
tln.db
.insert_version(
"fallback",
vid,
&talon::db::model::Version {
fallback,
spa,
latest_version: Some(1),
..Default::default()
},
)
.unwrap();
assert_eq!(
tln.db
.insert_version(
SUBDOMAIN,
&talon::db::model::Version {
fallback,
spa,
..Default::default()
},
)
.unwrap(),
1
);
tln.storage
.insert_file(
path!("tests" / "testfiles" / "ThetaDev0" / "index.html"),
vid,
SUBDOMAIN,
1,
"index.html",
)
.unwrap();
tln.storage
.insert_file(
path!("tests" / "testfiles" / "ThetaDev1" / "index.html"),
vid,
SUBDOMAIN,
1,
"fallback.html",
)
.unwrap();

6787
ui/menu/package-lock.json generated

File diff suppressed because it is too large Load diff