mirror of
https://github.com/runebaas/MemosFS.git
synced 2025-06-20 02:44:19 +02:00
Initial commit
This commit is contained in:
commit
deb3191bec
7 changed files with 2784 additions and 0 deletions
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
/target
|
||||||
|
/test
|
||||||
|
/memos
|
2195
Cargo.lock
generated
Normal file
2195
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
24
Cargo.toml
Normal file
24
Cargo.toml
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
[package]
|
||||||
|
name = "memosfs"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
# Fuse
|
||||||
|
fuser = { version = "0.15", features = ["serde"] }
|
||||||
|
|
||||||
|
# Platform
|
||||||
|
nix = { version = "0.30", features = ["user"] }
|
||||||
|
|
||||||
|
# Error Handling
|
||||||
|
anyhow = "1"
|
||||||
|
|
||||||
|
# Data
|
||||||
|
serde = { version = "1", features = ["derive"] }
|
||||||
|
short-uuid = "0.2"
|
||||||
|
uuid = "1"
|
||||||
|
chrono = { version = "0.4", features = ["serde"]}
|
||||||
|
moka = { version = "0.12", features = ["sync"] }
|
||||||
|
|
||||||
|
# Web
|
||||||
|
reqwest = { version = "0.12", features = ["blocking", "json"] }
|
21
LICENSE
Normal file
21
LICENSE
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2025 Daan Boerlage
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
43
readme.md
Normal file
43
readme.md
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
# MemosFS
|
||||||
|
|
||||||
|
A fuse filesystem from [Memos](https://www.usememos.com/).
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
`./memosfs <mountpoint> <api_base> <access_token>`
|
||||||
|
|
||||||
|
| parameter | example | description |
|
||||||
|
|----------------|---------------------------------|-------------------------------------------------------------------------|
|
||||||
|
| `mountpoint` | `./memos` | filesystem location to mount the filesystem |
|
||||||
|
| `api_base` | `https://memos.example.com/api` | base url of the API (no `/` at the end) |
|
||||||
|
| `access_token` | `eyJhbGciOiJIUz...` | an [access token](https://www.usememos.com/docs/security/access-tokens) |
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- [x] List and read memos
|
||||||
|
- [ ] Editing memos
|
||||||
|
- [ ] Creating memos
|
||||||
|
- [ ] Group by tag
|
||||||
|
- [ ] Group by date
|
||||||
|
- [ ] Group by creator
|
||||||
|
- [ ] Comments
|
||||||
|
- [ ] Resources
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
* This has been tested with Memos v0.24.x
|
||||||
|
* Only linux is supported
|
||||||
|
* Memos are cached for 30 seconds
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
|
**Prerequisites**
|
||||||
|
|
||||||
|
* Rust 1.87 or newer
|
||||||
|
* libfuse
|
||||||
|
|
||||||
|
**Building**
|
||||||
|
|
||||||
|
```shell
|
||||||
|
cargo build --release
|
||||||
|
```
|
152
src/inodes.rs
Normal file
152
src/inodes.rs
Normal file
|
@ -0,0 +1,152 @@
|
||||||
|
use short_uuid::CustomTranslator;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::hash::{DefaultHasher, Hash, Hasher};
|
||||||
|
|
||||||
|
/// Memos is written in go and use https://github.com/lithammer/shortuuid for uid generation.
|
||||||
|
/// Considering that these are just uuids, we can get an u128 from them.
|
||||||
|
struct ShortUuidConverter {
|
||||||
|
translator: CustomTranslator,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ShortUuidConverter {
|
||||||
|
/// The go library uses a custom alphabet, so we need to use that too.
|
||||||
|
/// Taken from https://github.com/lithammer/shortuuid/blob/ffec47be5937285a5ee628d94b2dcbb8cb58e22d/alphabet.go#L12
|
||||||
|
const SHORT_UID_ALPHA: &'static str =
|
||||||
|
"23456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
|
||||||
|
|
||||||
|
fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
translator: CustomTranslator::new(Self::SHORT_UID_ALPHA).unwrap(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn try_to_u128(&self, input: &str) -> anyhow::Result<u128> {
|
||||||
|
let short = match short_uuid::ShortUuidCustom::parse_str(input, &self.translator) {
|
||||||
|
Ok(short) => short,
|
||||||
|
Err(reason) => return Err(anyhow::anyhow!("Failed to parse {}: {}", input, reason)),
|
||||||
|
};
|
||||||
|
let uid = short.to_uuid(&self.translator).unwrap();
|
||||||
|
Ok(uid.as_u128())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn from_u128(&self, input: u128) -> String {
|
||||||
|
let uid = uuid::Uuid::from_u128(input);
|
||||||
|
let short = short_uuid::ShortUuidCustom::from_uuid(&uid, &self.translator);
|
||||||
|
short.to_string()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct INodeManager {
|
||||||
|
nodes: HashMap<u64, u128>,
|
||||||
|
short_uuid_converter: ShortUuidConverter,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl INodeManager {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
nodes: HashMap::new(),
|
||||||
|
short_uuid_converter: ShortUuidConverter::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_or_create_inode(&mut self, uid: &str) -> u64 {
|
||||||
|
let num = self.short_uuid_converter.try_to_u128(uid).unwrap();
|
||||||
|
let inode = self.hash_num(num);
|
||||||
|
self.nodes.insert(inode, num);
|
||||||
|
inode
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_uid(&self, inode: u64) -> Option<String> {
|
||||||
|
match self.nodes.get(&inode).copied() {
|
||||||
|
None => None,
|
||||||
|
Some(num) => Some(self.short_uuid_converter.from_u128(num)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn hash_num(&self, num: u128) -> u64 {
|
||||||
|
let mut hasher = DefaultHasher::new();
|
||||||
|
num.hash(&mut hasher);
|
||||||
|
hasher.finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
/// (original_uid, expected_uuid_u128, expected_inode)
|
||||||
|
const TEST_IDS: [(&str, u128, u64); 10] = [
|
||||||
|
(
|
||||||
|
"VgnhpfGXw7vHWPce6oNaVS",
|
||||||
|
206797227597459323123527397586500652966,
|
||||||
|
3913517761545603567,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"hqKG23Ds8hiZXziKorZvHH",
|
||||||
|
297564317390508405213354428528166123258,
|
||||||
|
10895052826299621117,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"JBSRgn6FcAP3s3S8WHC6Wy",
|
||||||
|
120769892440230658418002098667456834855,
|
||||||
|
925797026661051322,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"Bp7Xgaz3rs6F5e9SddHLcP",
|
||||||
|
73279780794183508306623012303071573694,
|
||||||
|
16832634417085068821,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"ELoMHpye6sqJKceTTZmyCH",
|
||||||
|
92114081947781017664946393632646945141,
|
||||||
|
3318364556962759386,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"7CppGL2gXD7meWV2GLFnLZ",
|
||||||
|
38772739364293647929273214809052070870,
|
||||||
|
17639521134799251273,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"PKK3i89cZZ3ELGjER85KND",
|
||||||
|
159155872788710468840620268141821518047,
|
||||||
|
5604479176214409706,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"bpdrRf6Y4w8X9BAL4TJZV6",
|
||||||
|
252650725998566377086795281031975766693,
|
||||||
|
6088146008880505408,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"SHtDbXtL7M7iyTnSQaMyrk",
|
||||||
|
181382667582346113605641996269296312097,
|
||||||
|
15579490772996943984,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"GA8XqyXDNFEko6nW5UFbHh",
|
||||||
|
105655915756677130982417952151348309032,
|
||||||
|
5472690243812647263,
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn convert_short_uids() {
|
||||||
|
let converter = ShortUuidConverter::new();
|
||||||
|
for (input, expected, _) in TEST_IDS {
|
||||||
|
let num = converter.try_to_u128(input).unwrap();
|
||||||
|
assert_eq!(num, expected, "Failed to decode {}", input);
|
||||||
|
let enc = converter.from_u128(num);
|
||||||
|
assert_eq!(enc, input, "Failed to encode {}", num);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn inode_converter() {
|
||||||
|
let mut inode_manager = INodeManager::new();
|
||||||
|
for (input, _, expected) in TEST_IDS {
|
||||||
|
let num = inode_manager.get_or_create_inode(input);
|
||||||
|
assert_eq!(num, expected, "Failed to decode {}", input);
|
||||||
|
let enc = inode_manager.get_uid(num);
|
||||||
|
assert_eq!(enc.unwrap(), input, "Failed to encode {}", num);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
346
src/main.rs
Normal file
346
src/main.rs
Normal file
|
@ -0,0 +1,346 @@
|
||||||
|
mod inodes;
|
||||||
|
|
||||||
|
use fuser::{
|
||||||
|
FileAttr, FileType, Filesystem, MountOption, ReplyAttr, ReplyData, ReplyDirectory, ReplyEntry,
|
||||||
|
Request,
|
||||||
|
};
|
||||||
|
use nix::libc::{EIO, ENOENT};
|
||||||
|
use reqwest::Method;
|
||||||
|
use serde::Deserialize;
|
||||||
|
use std::ffi::OsStr;
|
||||||
|
use std::time::{Duration, UNIX_EPOCH};
|
||||||
|
|
||||||
|
const BASE_TTL: Duration = Duration::from_secs(30);
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Deserialize)]
|
||||||
|
pub struct MemoResponse {
|
||||||
|
pub memos: Vec<Memo>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct Memo {
|
||||||
|
pub name: String,
|
||||||
|
pub state: String,
|
||||||
|
pub creator: String,
|
||||||
|
pub create_time: chrono::DateTime<chrono::Utc>,
|
||||||
|
pub update_time: Option<chrono::DateTime<chrono::Utc>>,
|
||||||
|
pub display_time: Option<chrono::DateTime<chrono::Utc>>,
|
||||||
|
pub content: String,
|
||||||
|
pub visibility: String,
|
||||||
|
pub tags: Vec<String>,
|
||||||
|
pub snippet: String,
|
||||||
|
pub relations: Vec<MemoRelation>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Memo {
|
||||||
|
pub fn into_file_attr(self, ino: u64, uid: u32, gid: u32) -> FileAttr {
|
||||||
|
let len = self.content.len() as u64;
|
||||||
|
FileAttr {
|
||||||
|
ino,
|
||||||
|
size: len,
|
||||||
|
blocks: f64::ceil(len as f64 / 512.0) as u64,
|
||||||
|
atime: self.display_time.unwrap_or(self.create_time).into(),
|
||||||
|
mtime: self.update_time.unwrap_or(self.create_time).into(),
|
||||||
|
ctime: self.update_time.unwrap_or(self.create_time).into(),
|
||||||
|
crtime: self.create_time.into(),
|
||||||
|
kind: FileType::RegularFile,
|
||||||
|
perm: 0o644,
|
||||||
|
nlink: 1,
|
||||||
|
uid,
|
||||||
|
gid,
|
||||||
|
rdev: 0,
|
||||||
|
flags: 0,
|
||||||
|
blksize: 512,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct MemoRelation {
|
||||||
|
memo: MemoRelationMemo,
|
||||||
|
related_memo: MemoRelationMemo,
|
||||||
|
#[serde(rename = "type")]
|
||||||
|
relation_type: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct MemoRelationMemo {
|
||||||
|
name: String,
|
||||||
|
uid: String,
|
||||||
|
snippet: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_dir_attr(ino: u64, uid: u32, gid: u32) -> FileAttr {
|
||||||
|
FileAttr {
|
||||||
|
ino,
|
||||||
|
size: 0,
|
||||||
|
blocks: 0,
|
||||||
|
atime: UNIX_EPOCH,
|
||||||
|
mtime: UNIX_EPOCH,
|
||||||
|
ctime: UNIX_EPOCH,
|
||||||
|
crtime: UNIX_EPOCH,
|
||||||
|
kind: FileType::Directory,
|
||||||
|
perm: 0o755,
|
||||||
|
nlink: 2,
|
||||||
|
uid,
|
||||||
|
gid,
|
||||||
|
rdev: 0,
|
||||||
|
flags: 0,
|
||||||
|
blksize: 512,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct MemosFS {
|
||||||
|
api_base: String,
|
||||||
|
token: String,
|
||||||
|
inode_manager: inodes::INodeManager,
|
||||||
|
cache: moka::sync::Cache<String, Memo>,
|
||||||
|
all_cache: moka::sync::Cache<String, MemoResponse>,
|
||||||
|
uid: u32,
|
||||||
|
gid: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MemosFS {
|
||||||
|
fn new(api_base: &str, token: &str) -> Self {
|
||||||
|
MemosFS {
|
||||||
|
api_base: api_base.to_string(),
|
||||||
|
token: token.to_string(),
|
||||||
|
inode_manager: inodes::INodeManager::new(),
|
||||||
|
cache: moka::sync::CacheBuilder::new(500)
|
||||||
|
.time_to_live(Duration::from_secs(30))
|
||||||
|
.build(),
|
||||||
|
all_cache: moka::sync::CacheBuilder::new(5)
|
||||||
|
.time_to_live(Duration::from_secs(30))
|
||||||
|
.build(),
|
||||||
|
uid: nix::unistd::geteuid().as_raw(),
|
||||||
|
gid: nix::unistd::getegid().as_raw(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_memo_from_id(&self, id: &str) -> Option<Memo> {
|
||||||
|
if let Some(memo) = self.cache.get(id) {
|
||||||
|
return Some(memo);
|
||||||
|
}
|
||||||
|
|
||||||
|
println!("fetching {}", id);
|
||||||
|
|
||||||
|
let url = format!("{}/v1/memos/{}", self.api_base, id);
|
||||||
|
let mut req =
|
||||||
|
reqwest::blocking::Request::new(Method::GET, reqwest::Url::parse(&url).unwrap());
|
||||||
|
req.headers_mut().insert(
|
||||||
|
"Authorization",
|
||||||
|
format!("Bearer {}", self.token).parse().unwrap(),
|
||||||
|
);
|
||||||
|
let res = reqwest::blocking::Client::new().execute(req).unwrap();
|
||||||
|
|
||||||
|
match res.json::<Memo>() {
|
||||||
|
Ok(memo) => {
|
||||||
|
self.cache.insert(id.to_string(), memo.clone());
|
||||||
|
Some(memo)
|
||||||
|
}
|
||||||
|
Err(reason) => {
|
||||||
|
eprintln!("Failed to parse memo: {}", reason);
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Filesystem for MemosFS {
|
||||||
|
fn lookup(&mut self, _req: &Request, parent: u64, name: &OsStr, reply: ReplyEntry) {
|
||||||
|
if parent != 1 {
|
||||||
|
reply.error(ENOENT);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let name = match name.to_str() {
|
||||||
|
Some(x) => x,
|
||||||
|
None => {
|
||||||
|
reply.error(ENOENT);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if name.len() != 25 {
|
||||||
|
reply.error(ENOENT);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let name_without_ext = &name[0..22];
|
||||||
|
|
||||||
|
let ino = self.inode_manager.get_or_create_inode(name_without_ext);
|
||||||
|
|
||||||
|
match self.get_memo_from_id(&name_without_ext) {
|
||||||
|
Some(memo) => {
|
||||||
|
let attr = memo.into_file_attr(ino, self.uid, self.gid);
|
||||||
|
reply.entry(&BASE_TTL, &attr, 0)
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
reply.error(ENOENT);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn getattr(&mut self, _req: &Request, ino: u64, _fh: Option<u64>, reply: ReplyAttr) {
|
||||||
|
if ino == 1 {
|
||||||
|
reply.attr(&BASE_TTL, &get_dir_attr(1, self.uid, self.gid));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let id = match self.inode_manager.get_uid(ino) {
|
||||||
|
Some(id) => id,
|
||||||
|
None => {
|
||||||
|
reply.error(ENOENT);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
match self.get_memo_from_id(&id) {
|
||||||
|
Some(memo) => {
|
||||||
|
let attr = memo.into_file_attr(ino, self.uid, self.gid);
|
||||||
|
reply.attr(&BASE_TTL, &attr)
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
reply.error(ENOENT);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read(
|
||||||
|
&mut self,
|
||||||
|
_req: &Request,
|
||||||
|
ino: u64,
|
||||||
|
_fh: u64,
|
||||||
|
offset: i64,
|
||||||
|
_size: u32,
|
||||||
|
_flags: i32,
|
||||||
|
_lock: Option<u64>,
|
||||||
|
reply: ReplyData,
|
||||||
|
) {
|
||||||
|
let id = match self.inode_manager.get_uid(ino) {
|
||||||
|
Some(id) => id,
|
||||||
|
None => {
|
||||||
|
reply.error(ENOENT);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
match self.get_memo_from_id(&id) {
|
||||||
|
Some(memo) => {
|
||||||
|
reply.data(&memo.content.as_bytes()[offset as usize..]);
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
reply.error(ENOENT);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn readdir(
|
||||||
|
&mut self,
|
||||||
|
_req: &Request,
|
||||||
|
ino: u64,
|
||||||
|
_fh: u64,
|
||||||
|
offset: i64,
|
||||||
|
mut reply: ReplyDirectory,
|
||||||
|
) {
|
||||||
|
if ino != 1 {
|
||||||
|
reply.error(ENOENT);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if offset == 0 {
|
||||||
|
if reply.add(1, 1, FileType::Directory, ".") {
|
||||||
|
reply.ok();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if offset <= 1 {
|
||||||
|
if reply.add(1, 2, FileType::Directory, "..") {
|
||||||
|
reply.ok();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if offset >= 2 {
|
||||||
|
let entries = match self.all_cache.get("all") {
|
||||||
|
Some(all) => all,
|
||||||
|
None => {
|
||||||
|
println!("Fetching all memos");
|
||||||
|
let uri = format!("{}/v1/memos?page_size=100", self.api_base);
|
||||||
|
let mut req = reqwest::blocking::Request::new(
|
||||||
|
Method::GET,
|
||||||
|
reqwest::Url::parse(&uri).unwrap(),
|
||||||
|
);
|
||||||
|
req.headers_mut().insert(
|
||||||
|
"Authorization",
|
||||||
|
format!("Bearer {}", self.token).parse().unwrap(),
|
||||||
|
);
|
||||||
|
|
||||||
|
let res = match reqwest::blocking::Client::new().execute(req) {
|
||||||
|
Ok(res) => res,
|
||||||
|
Err(_) => {
|
||||||
|
reply.error(EIO);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let entries = match res.json::<MemoResponse>() {
|
||||||
|
Ok(entries) => entries,
|
||||||
|
Err(_) => {
|
||||||
|
reply.error(EIO);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
self.all_cache.insert("all".to_string(), entries.clone());
|
||||||
|
|
||||||
|
entries
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
for (memo_index, entry) in entries.memos.into_iter().enumerate() {
|
||||||
|
let current_offset = (memo_index + 2) as i64;
|
||||||
|
|
||||||
|
if current_offset < offset {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let name = entry.name.split('/').last().unwrap();
|
||||||
|
let inode = self.inode_manager.get_or_create_inode(name);
|
||||||
|
let next_offset = current_offset + 1;
|
||||||
|
|
||||||
|
self.cache.insert(name.to_string(), entry.clone());
|
||||||
|
|
||||||
|
if reply.add(
|
||||||
|
inode,
|
||||||
|
next_offset,
|
||||||
|
FileType::RegularFile,
|
||||||
|
format!("{}.md", name),
|
||||||
|
) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
reply.ok();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
let args: Vec<String> = std::env::args().collect();
|
||||||
|
if args.len() != 4 {
|
||||||
|
eprintln!("Usage: {} <mountpoint> <api_base> <access_token>", args[0]);
|
||||||
|
std::process::exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mountpoint = &args[1];
|
||||||
|
let api_base = &args[2];
|
||||||
|
let access_token = &args[3];
|
||||||
|
let options = vec![MountOption::RO, MountOption::FSName("memosfs".to_string())];
|
||||||
|
|
||||||
|
fuser::mount2(MemosFS::new(api_base, access_token), mountpoint, &options).unwrap();
|
||||||
|
}
|
Loading…
Add table
Add a link
Reference in a new issue