Skip to main content

squigit/
storage.rs

1// Copyright 2026 a7mddra
2// SPDX-License-Identifier: Apache-2.0
3
4//! Storage access shared by every Squigit shell-facing service.
5
6use std::collections::HashMap;
7use std::fs;
8use std::path::PathBuf;
9
10pub use squigit_storage::*;
11
12pub(crate) fn profile_store() -> Result<ProfileStore> {
13    ProfileStore::new()
14}
15
16pub(crate) fn thread_store() -> Result<ThreadStorage> {
17    ThreadStorage::new()
18}
19
20pub(crate) fn version_store() -> Result<VersionStore> {
21    VersionStore::new()
22}
23
24pub(crate) fn config_path(file_name: &str) -> Option<PathBuf> {
25    paths::base_config_dir().map(|directory| directory.join(file_name))
26}
27
28pub fn rules_path() -> Option<PathBuf> {
29    rules::rules_path()
30}
31
32pub fn load_rules() -> std::result::Result<String, String> {
33    let path =
34        rules_path().ok_or_else(|| "Could not locate Squigit's RULES.md path".to_string())?;
35    match fs::read_to_string(path) {
36        Ok(content) => Ok(content),
37        Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(String::new()),
38        Err(error) => Err(error.to_string()),
39    }
40}
41
42pub fn save_rules(content: &str) -> std::result::Result<(), String> {
43    if rules_path().is_none() {
44        return Err("Could not locate Squigit's RULES.md path".to_string());
45    }
46    rules::save_rules(content)
47}
48
49/// Store image bytes in Squigit's content-addressable storage.
50pub fn store_image(bytes: &[u8], explicit_tone: Option<String>) -> Result<StoredImage> {
51    thread_store()?.store_image(bytes, explicit_tone)
52}
53
54/// Store arbitrary bytes in Squigit's content-addressable storage.
55pub fn store_file(
56    bytes: &[u8],
57    extension: &str,
58    explicit_tone: Option<String>,
59) -> Result<StoredImage> {
60    thread_store()?.store_file(bytes, extension, explicit_tone)
61}
62
63#[derive(Clone, Debug)]
64pub struct GalleryThread {
65    pub thread_id: String,
66    pub title: String,
67    pub updated_at: String,
68}
69
70#[derive(Clone, Debug)]
71pub struct GalleryImage {
72    pub hash: String,
73    pub path: String,
74    pub updated_at: String,
75    pub threads: Vec<GalleryThread>,
76}
77
78/// Return stored thread images grouped for gallery presentation.
79pub fn list_gallery(offset: u32, limit: u32) -> Result<Vec<GalleryImage>> {
80    let storage = thread_store()?;
81    let mut grouped: HashMap<String, Vec<GalleryThread>> = HashMap::new();
82
83    for thread in storage.list_threads()? {
84        if thread.image_hash.is_empty() || thread.image_hash == EMPTY_STATE_ASSET_ID {
85            continue;
86        }
87        grouped
88            .entry(thread.image_hash)
89            .or_default()
90            .push(GalleryThread {
91                thread_id: thread.id,
92                title: thread.title,
93                updated_at: thread.updated_at.to_rfc3339(),
94            });
95    }
96
97    let mut images = grouped
98        .into_iter()
99        .filter_map(|(hash, mut threads)| {
100            threads.sort_by(|left, right| right.updated_at.cmp(&left.updated_at));
101            let updated_at = threads.first()?.updated_at.clone();
102            let path = storage.get_image_path(&hash).ok()?;
103            Some(GalleryImage {
104                hash,
105                path,
106                updated_at,
107                threads,
108            })
109        })
110        .collect::<Vec<_>>();
111    images.sort_by(|left, right| right.updated_at.cmp(&left.updated_at));
112    Ok(images
113        .into_iter()
114        .skip(offset as usize)
115        .take((limit as usize).clamp(1, 100))
116        .collect())
117}