From 530d56bcb2b481c93deef05256afae3e4d87c0ca Mon Sep 17 00:00:00 2001 From: AB Date: Wed, 15 Apr 2020 01:55:02 +0300 Subject: [PATCH] Ititial commit. It works somehow. --- .gitignore | 2 + .vscode/launch.json | 43 ++++++++ .vscode/tasks.json | 22 ++++ Cargo.toml | 17 +++ src/main.rs | 244 ++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 328 insertions(+) create mode 100644 .gitignore create mode 100644 .vscode/launch.json create mode 100644 .vscode/tasks.json create mode 100644 Cargo.toml create mode 100644 src/main.rs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..96ef6c0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/target +Cargo.lock diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..cc08282 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,43 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "type": "lldb", + "env": {"RUST_BACKTRACE": "1"}, + "request": "launch", + "name": "Debug executable 'musfuse'", + "cargo": { + "args": [ + "run", + ], + }, + "args": [ + "mnt" + ], + "cwd": "${workspaceFolder}", + "postDebugTask": "Umount FUSE", + }, + { + "type": "lldb", + "request": "launch", + "name": "Debug unit tests in executable 'musfuse'", + "cargo": { + "args": [ + "test", + "--no-run", + "--bin=musfuse", + "--package=musfuse" + ], + "filter": { + "name": "musfuse", + "kind": "bin" + } + }, + "args": [], + "cwd": "${workspaceFolder}" + } + ] +} \ No newline at end of file diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 0000000..b5aef37 --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,22 @@ +{ + // See https://go.microsoft.com/fwlink/?LinkId=733558 + // for the documentation about the tasks.json format + "version": "2.0.0", + "tasks": [ + { + "type": "cargo", + "subcommand": "build", + "problemMatcher": [ + "$rustc" + ], + "group": "build" + }, + { + "label": "Umount FUSE", + "type": "shell", + "command": "fusermount -u mnt", + "group": "build", + "problemMatcher": [] + } + ] +} \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..880ceb3 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "musfuse" +version = "0.1.0" +authors = ["AB "] +edition = "2018" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +[dependencies] +reqwest = { version = "0.10", features = ["json", "blocking"] } +tokio = { version = "0.2", features = ["full"] } +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +percent-encoding = "*" +fuse = "*" +time = "0.1" +libc = "*" +rustc-serialize = "*" diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..4457225 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,244 @@ +// Fuse staff +#[cfg(target_family = "unix")] +extern crate fuse; +extern crate libc; +extern crate time; + +#[cfg(target_family = "unix")] +use fuse::{ + FileAttr, FileType, Filesystem, ReplyAttr, ReplyData, ReplyDirectory, ReplyEntry, Request, +}; +#[cfg(target_family = "unix")] +use libc::ENOENT; +#[cfg(target_family = "unix")] +use std::collections::BTreeMap; +#[cfg(target_family = "unix")] +use std::env; +#[cfg(target_family = "unix")] +use std::ffi::OsStr; +#[cfg(target_family = "unix")] +use time::Timespec; + +// Download lib staff +use percent_encoding::percent_decode_str; +use serde::Deserialize; +use std::collections::HashMap; +use std::fs; +use std::path::Path; + +#[derive(Default, Debug, Clone, PartialEq, Deserialize)] +pub struct Track { + pub id: Option, + pub name: Option, + pub artist: Option, + pub album: Option, + pub genre: Option, + pub year: Option, + pub format: Option, + pub filetype: Option, + pub path: Option, +} + +const API_URL: &str = "https://mus.hexor.ru"; + +fn get_basename(path: Option<&String>) -> Option { + let base = match percent_decode_str(path.unwrap().as_str()).decode_utf8() { + Ok(path) => { + let remote_name = path.into_owned(); + let basename = Path::new(&remote_name).file_name(); + match basename { + Some(name) => Some(name.to_os_string().into_string().unwrap()), + None => None, + } + } + Err(_) => None, + }; + base +} + +#[tokio::main] +async fn get_tracks() -> Result, Box> { + let resp = reqwest::get(format!("{}/songs", API_URL).as_str()) + .await? + .json::>() + .await?; + println!("Found {} tracks.", resp.len()); + Ok(resp) +} + +#[cfg(target_family = "unix")] +struct JsonFilesystem { + tree: Vec, + attrs: BTreeMap, + inodes: BTreeMap, + buffer_data: Vec, + buffer_name: String, +} + +#[cfg(target_family = "unix")] +impl JsonFilesystem { + fn new(tree: &Vec) -> JsonFilesystem { + let mut attrs = BTreeMap::new(); + let mut inodes = BTreeMap::new(); + let ts = time::now().to_timespec(); + let attr = FileAttr { + ino: 1, + size: 0, + blocks: 0, + atime: ts, + mtime: ts, + ctime: ts, + crtime: ts, + kind: FileType::Directory, + perm: 0o755, + nlink: 0, + uid: 0, + gid: 0, + rdev: 0, + flags: 0, + }; + attrs.insert(1, attr); + inodes.insert("/".to_string(), 1); + for (i, track) in tree.iter().enumerate() { + let basename = get_basename(track.path.as_ref()).unwrap().to_string(); + let attr = FileAttr { + ino: i as u64 + 2, + size: 1024 * 1024 * 1024 as u64, + blocks: 0, + atime: ts, + mtime: ts, + ctime: ts, + crtime: ts, + kind: FileType::RegularFile, + perm: 0o644, + nlink: 0, + uid: 0, + gid: 0, + rdev: 0, + flags: 0, + }; + attrs.insert(attr.ino, attr); + inodes.insert(basename.clone(), attr.ino); + } + JsonFilesystem { + tree: tree.clone(), + attrs: attrs, + inodes: inodes, + buffer_data: Vec::new(), + buffer_name: "".to_string(), + } + } +} + +#[cfg(target_family = "unix")] +impl Filesystem for JsonFilesystem { + fn getattr(&mut self, _req: &Request, ino: u64, reply: ReplyAttr) { + println!("getattr(ino={})", ino); + match self.attrs.get(&ino) { + Some(attr) => { + let ttl = Timespec::new(1, 0); + reply.attr(&ttl, attr); + } + None => reply.error(ENOENT), + }; + } + + fn lookup(&mut self, _req: &Request, parent: u64, name: &OsStr, reply: ReplyEntry) { + println!("lookup(parent={}, name={})", parent, name.to_str().unwrap()); + let inode = match self.inodes.get(name.to_str().unwrap()) { + Some(inode) => inode, + None => { + reply.error(ENOENT); + return; + } + }; + match self.attrs.get(inode) { + Some(attr) => { + let ttl = Timespec::new(1, 0); + reply.entry(&ttl, attr, 0); + } + None => reply.error(ENOENT), + }; + } + + fn read( + &mut self, + _req: &Request, + ino: u64, + fh: u64, + offset: i64, + size: u32, + reply: ReplyData, + ) { + println!( + "read(ino={}, fh={}, offset={}, size={})", + ino, fh, offset, size + ); + //let mus = fs::read("/home/ab/Downloads/Mizuki.mp3").unwrap(); + let url = &self.tree[(ino - 2) as usize].path.as_ref().unwrap(); + let full_url = format!("{}/{}", API_URL, url); + let mut full_track: Vec = Vec::new(); + if self.buffer_name == full_url { + full_track = self.buffer_data.clone(); + println!("Hit cache!"); + } else { + let resp = reqwest::blocking::get(full_url.as_str()).unwrap(); + let test = resp.bytes().unwrap(); + full_track = test.to_vec().clone(); + self.buffer_data = full_track.clone(); + self.buffer_name = full_url; + println!("Miss cache!"); + } + let mut chunk_end = size as usize + offset as usize; + + if chunk_end >= full_track.len() { + chunk_end = full_track.len() - 1; + } + if offset as usize >= full_track.len() { + reply.data(&full_track[(full_track.len() - 1) as usize..chunk_end as usize]); + } else { + reply.data(&full_track[offset as usize..chunk_end as usize]); + } + println!("Len: {}, chunk end {}", full_track.len(), chunk_end); + return + } + + fn readdir( + &mut self, + _req: &Request, + ino: u64, + fh: u64, + offset: i64, + mut reply: ReplyDirectory, + ) { + println!("readdir(ino={}, fh={}, offset={})", ino, fh, offset); + if ino == 1 { + if offset == 0 { + reply.add(1, 0, FileType::Directory, "."); + reply.add(1, 1, FileType::Directory, ".."); + } + for (i, (key, &inode)) in self.inodes.iter().enumerate().skip(offset as usize) { + if inode == 1 { + continue; + } + reply.add(inode, (i + 1) as i64, FileType::RegularFile, key); + } + reply.ok(); + } else { + reply.error(ENOENT); + } + } +} + +fn main() { + let lib = get_tracks().unwrap(); + let fs = JsonFilesystem::new(&lib); + let mountpoint = match env::args().nth(1) { + Some(path) => path, + None => { + println!("Usage: {} ", env::args().nth(0).unwrap()); + return; + } + }; + fuse::mount(fs, &mountpoint, &[]).expect("Couldn't mount filesystem"); +}