summary refs log tree commit diff
path: root/src/data/content.rs
blob: ecd985f0d920a6a0e45d08138a3ce8340ce65104 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
use std::fs;
use std::collections::HashMap;
use std::path::{PathBuf, Path};
use std::sync::{Arc,RwLock};
use chrono::{DateTime, Utc};
use futures::{FutureExt, TryStreamExt};
use uuid::Uuid;
use fs2::FileExt;
use tokio::runtime;
use tokio_stream::wrappers::ReadDirStream;
use futures::stream::StreamExt;

#[derive(Hash, PartialEq, Eq, Clone)]
struct PageUuid(Uuid);
#[derive(Hash, PartialEq, Eq, Clone)]
struct NamespaceUuid(Uuid);
#[derive(Hash, PartialEq, Eq, Clone)]
struct MediaUuid(Uuid);

struct ContentSnapshot {
    pages: HashMap<PageUuid, Page>,
    namespaces: HashMap<NamespaceUuid, Namespace>,
    media: HashMap<MediaUuid, Media>,

    namespace_path: HashMap<String, NamespaceUuid>,
    page_path: HashMap<String, PageUuid>,
    media_path: HashMap<String, MediaUuid>,

    render_cache: HashMap<PageUuid, String>,
}

struct Page {
    uuid: PageUuid,
    title: String,
    namespace: NamespaceUuid,
    slug: String,
    current_version: DateTime<Utc>,
    prev_version: DateTime<Utc>,
}

struct Namespace {
    uuid: NamespaceUuid,
    path: String,
    pages: Vec<PageUuid>,
}

struct Media {
    uuid: MediaUuid,
    filename: String,
    mime_type: String,
    uploaded_by: Uuid,
    uploaded_on: Uuid,
    used_on: Vec<PageUuid>,
}

struct ContentController {
    snapshot: RwLock<Box<Arc<ContentSnapshot>>>,
    lock: fs::File,
}

impl ContentController {
    pub fn init(data_dir: PathBuf) -> Result<Self, String> {
        let lock_path = Path::join(&data_dir, ".lock");
        let lockfile = fs::OpenOptions::new()
            .read(true).write(true).create(true)
            .open(&lock_path)
            .map_err(|_| "Could not open data directory".to_string())?;

        lockfile.try_lock_exclusive()
            .map_err(|_| "Could not lock data directory".to_string())?;

        let runtime = runtime::Builder::new_multi_thread()
            .build()
            .map_err(|_| "Could not start async runtime".to_string())?;

        // Read the things
        let snapshot = runtime.block_on(Self::read_data(&data_dir))?;

        Ok(Self {
            lock: lockfile,
            snapshot: RwLock::new(Box::new(Arc::new(snapshot))),
        })
    }

    async fn read_data(data_dir: &PathBuf) -> Result<ContentSnapshot, String> {
        use tokio::fs;

        let page_slugs = Arc::new(tokio::sync::Mutex::new(HashMap::<PageUuid, String>::new()));

        let namespace_names_dir = Path::join(&data_dir, "namespaces/names");
        let namespace_ids_dir = Path::join(&data_dir, "namespaces/id");
        let namespace_future = fs::read_dir(&namespace_names_dir).await
            .map_err(|_| "Could not open namespace directory".to_string())
            .map(|dir_entries| { ReadDirStream::new(dir_entries) })?
            .filter_map(async |dir_entry| -> Option<Namespace> {
                let link_path = dir_entry.as_ref().ok()?.path();
                let target_path = dir_entry.as_ref().ok()?
                    .metadata().await.ok()?
                    .is_symlink()
                    .then_some(
                        fs::read_link(link_path).await.ok()
                    )??;

                let last_segment = target_path.file_name()?;
                target_path.parent()?
                    .eq(&namespace_ids_dir).then_some(())?;

                let namespace_name = dir_entry.as_ref().ok()?.file_name().to_str()?.to_string();
                let namespace_uuid = NamespaceUuid(Uuid::try_parse(last_segment.to_str()?).ok()?);

                let namespace_pages = fs::read_dir(Path::join(&namespace_ids_dir, last_segment).join("pages")).await.ok()?;
                let namespace_page_uuids = ReadDirStream::new(namespace_pages)
                    .filter_map(async |dir_entry| -> Option<PageUuid> {
                        let page_path = dir_entry.as_ref().ok()?.path();
                        let page_uuid = dir_entry.as_ref().ok()?
                            .metadata().await.ok()?
                            .is_symlink()
                            .then_some(
                                fs::read_link(&page_path).await.ok()
                            )??;

                        let page_uuid = PageUuid(Uuid::try_parse(&page_uuid.to_str()?).ok()?);
                        let page_slug = page_path.file_name()?.to_str()?.to_string();

                        page_slugs.lock().await.insert(page_uuid.clone(), page_slug);

                        Some(page_uuid)
                    }).collect::<Vec<PageUuid>>().await;

                Some(Namespace {
                    uuid: namespace_uuid,
                    path: namespace_name,
                    pages: namespace_page_uuids,
                })
            }).collect::<Vec<Namespace>>().await;

        let pages_dir = Path::join(&data_dir, "pages/id");
        let page_future = fs::read_dir(&pages_dir).await
            .map_err(|_| "Could not open pages data directory".to_string())
            .map(|dir_entries| { ReadDirStream::new(dir_entries) })?
            .filter_map(async |dir_entry| -> Option<Page> {

            });

        return Err("Unimplemented".to_string());

    }

}