diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 0a15f6d..63344d9 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -31,6 +31,9 @@ jobs: - name: Strip run: | strip target/x86_64-unknown-linux-musl/release/rfs + - name: Strip + run: | + strip target/x86_64-unknown-linux-musl/release/docker2fl - name: Create Release id: create_release uses: actions/create-release@v1 @@ -41,8 +44,8 @@ jobs: release_name: Release ${{ github.ref }} draft: false prerelease: false - - name: Upload Release Asset - id: upload-release-asset + - name: Upload Release Asset for RFS + id: upload-release-asset-rfs uses: actions/upload-release-asset@v1 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} @@ -51,3 +54,14 @@ jobs: asset_path: target/x86_64-unknown-linux-musl/release/rfs asset_name: rfs asset_content_type: application/x-pie-executable + + - name: Upload Release Asset for docker2fl + id: upload-release-asset-docker2fl + uses: actions/upload-release-asset@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: ${{ steps.create_release.outputs.upload_url }} + asset_path: target/x86_64-unknown-linux-musl/release/docker2fl + asset_name: docker2fl + asset_content_type: application/x-pie-executable diff --git a/.gitignore b/.gitignore index 4b4ab64..b9d184e 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,5 @@ /tests/*.flist.d result .direnv/ +fl-server/flists +fl-server/config.toml \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml index 0bc935d..d6e477e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,8 +3,8 @@ resolver = "2" members = [ "rfs", - "docker2fl", - "fl-server" + "docker2fl", + "fl-server" ] [profile.release] diff --git a/Dockerfile b/Dockerfile index 83aa851..5742114 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,13 +1,23 @@ -FROM rust:slim +FROM rust:slim as builder + +WORKDIR /src + +COPY fl-server /src/fl-server +COPY rfs /src/rfs +COPY docker2fl /src/docker2fl +COPY Cargo.toml . +COPY Cargo.lock . +COPY config.toml . RUN apt-get update && apt-get install curl build-essential libssl-dev musl-tools -y +RUN rustup target add x86_64-unknown-linux-musl +RUN cargo build --release --bin fl-server --target=x86_64-unknown-linux-musl -WORKDIR /myapp +FROM alpine:3.19 -COPY . . +WORKDIR /app -RUN rustup target add x86_64-unknown-linux-musl -RUN cargo build --release --target=x86_64-unknown-linux-musl +COPY --from=builder /src/target/x86_64-unknown-linux-musl/release/fl-server . +COPY --from=builder /src/config.toml . -CMD ["/myapp/target/x86_64-unknown-linux-musl/release/fl-server", "--config-path", "config.toml"] -EXPOSE 3000 +ENTRYPOINT [ "./fl-server", "--config-path", "config.toml"] diff --git a/docker2fl/src/main.rs b/docker2fl/src/main.rs index efa5cff..2895190 100644 --- a/docker2fl/src/main.rs +++ b/docker2fl/src/main.rs @@ -85,7 +85,7 @@ async fn main() -> Result<()> { }); let fl_name = docker_image.replace([':', '/'], "-") + ".fl"; - let meta = fungi::Writer::new(&fl_name).await?; + let meta = fungi::Writer::new(&fl_name, true).await?; let store = parse_router(&opts.store).await?; let container_name = Uuid::new_v4().to_string(); diff --git a/docs/README.md b/docs/README.md index 55ce36e..c01dbdb 100644 --- a/docs/README.md +++ b/docs/README.md @@ -4,7 +4,7 @@ The idea behind the FL format is to build a full filesystem description that is compact and also easy to use from almost ANY language. The format need to be easy to edit by tools like `rfs` or any other tool. -We decided to eventually use `sqlite`! Yes the `FL` file is just a `sqlite` database that has the following [schema](../schema/schema.sql) +We decided to eventually use `sqlite`! Yes the `FL` file is just a `sqlite` database that has the following [schema](../rfs/schema/schema.sql) ## Tables @@ -64,10 +64,10 @@ the `block` table is used to associate data file blocks with files. An `id` fiel the route table holds routing information for the blobs. It basically describe where to find `blobs` with certain `ids`. The routing is done as following: -> Note routing table is loaded one time when `rfs` is started and +> Note routing table is loaded one time when `rfs` is started. - We use the first byte of the blob `id` as the `route key` -- The `route key`` is then consulted against the routing table +- The `route key` is then consulted against the routing table - While building an `FL` all matching stores are updated with the new blob. This is how the system does replication - On `getting` an object, the list of matching routes are tried in random order the first one to return a value is used - Note that same range and overlapping ranges are allowed, this is how shards and replications are done. diff --git a/fl-server/src/config.rs b/fl-server/src/config.rs index d5e3597..677ebf8 100644 --- a/fl-server/src/config.rs +++ b/fl-server/src/config.rs @@ -29,7 +29,7 @@ pub struct AppState { #[derive(Debug, Default, Clone, Deserialize)] pub struct Config { pub host: String, - pub port: usize, + pub port: u16, pub store_url: Vec, pub flist_dir: String, @@ -44,14 +44,7 @@ pub async fn parse_config(filepath: &str) -> Result { let c: Config = toml::from_str(&content).context("failed to convert toml config data")?; if !hostname_validator::is_valid(&c.host) { - return Err(anyhow::Error::msg(format!("host '{}' is invalid", c.host))); - } - - if c.port > 65535 { - return Err(anyhow::Error::msg(format!( - "port '{}' is invalid, must be between [0, 65535]", - c.port - ))); + anyhow::bail!("host '{}' is invalid", c.host) } rfs::store::parse_router(&c.store_url) @@ -60,10 +53,10 @@ pub async fn parse_config(filepath: &str) -> Result { fs::create_dir_all(&c.flist_dir).context("failed to create flists directory")?; if c.jwt_expire_hours < 1 || c.jwt_expire_hours > 24 { - return Err(anyhow::Error::msg(format!( + anyhow::bail!(format!( "jwt expiry interval in hours '{}' is invalid, must be between [1, 24]", c.jwt_expire_hours - ))); + )) } Ok(c) diff --git a/fl-server/src/db.rs b/fl-server/src/db.rs index 3865b1d..9c3ef4f 100644 --- a/fl-server/src/db.rs +++ b/fl-server/src/db.rs @@ -1,3 +1,5 @@ +use std::collections::HashMap; + use serde::{Deserialize, Serialize}; use utoipa::ToSchema; @@ -8,24 +10,27 @@ pub struct User { } pub trait DB: Send + Sync { - fn get_user_by_username(&self, username: &str) -> Option<&User>; + fn get_user_by_username(&self, username: &str) -> Option; } #[derive(Debug, ToSchema)] -pub struct VecDB { - users: Vec, +pub struct MapDB { + users: HashMap, } -impl VecDB { +impl MapDB { pub fn new(users: &[User]) -> Self { Self { - users: users.to_vec(), + users: users + .iter() + .map(|u| (u.username.clone(), u.to_owned())) + .collect(), } } } -impl DB for VecDB { - fn get_user_by_username(&self, username: &str) -> Option<&User> { - self.users.iter().find(|u| u.username == username) +impl DB for MapDB { + fn get_user_by_username(&self, username: &str) -> Option { + self.users.get(username).cloned() } } diff --git a/fl-server/src/handlers.rs b/fl-server/src/handlers.rs index 0a0d58a..c5724d8 100644 --- a/fl-server/src/handlers.rs +++ b/fl-server/src/handlers.rs @@ -115,12 +115,11 @@ pub async fn create_flist_handler( return Err(ResponseError::Conflict("flist already exists".to_string())); } - let created = fs::create_dir_all(&username_dir); - if created.is_err() { + if let Err(err) = fs::create_dir_all(&username_dir) { log::error!( "failed to create user flist directory `{:?}` with error {:?}", &username_dir, - created.err() + err ); return Err(ResponseError::InternalServerError); } diff --git a/fl-server/src/main.rs b/fl-server/src/main.rs index cebde0d..e2dba06 100644 --- a/fl-server/src/main.rs +++ b/fl-server/src/main.rs @@ -72,7 +72,7 @@ async fn app() -> Result<()> { .await .context("failed to parse config file")?; - let db = Arc::new(db::VecDB::new(&config.users)); + let db = Arc::new(db::MapDB::new(&config.users.clone())); let app_state = Arc::new(config::AppState { jobs_state: Mutex::new(HashMap::new()), diff --git a/fl-server/src/response.rs b/fl-server/src/response.rs index ee3bcc8..193516f 100644 --- a/fl-server/src/response.rs +++ b/fl-server/src/response.rs @@ -56,9 +56,7 @@ impl IntoResponse for ResponseResult { ResponseResult::SignedIn(token) => { (StatusCode::CREATED, Json(serde_json::json!(token))).into_response() } - ResponseResult::FlistCreated(job) => { - (StatusCode::CREATED, Json(serde_json::json!(job))).into_response() - } + ResponseResult::FlistCreated(job) => (StatusCode::CREATED, Json(job)).into_response(), ResponseResult::FlistState(flist_state) => ( StatusCode::OK, Json(serde_json::json!({ @@ -66,9 +64,7 @@ impl IntoResponse for ResponseResult { })), ) .into_response(), - ResponseResult::Flists(flists) => { - (StatusCode::OK, Json(serde_json::json!(flists))).into_response() - } + ResponseResult::Flists(flists) => (StatusCode::OK, Json(flists)).into_response(), } } } diff --git a/fl-server/src/serve_flists.rs b/fl-server/src/serve_flists.rs index 0285421..d7ae9de 100644 --- a/fl-server/src/serve_flists.rs +++ b/fl-server/src/serve_flists.rs @@ -4,7 +4,7 @@ use axum::{ response::{Html, Response}, }; use serde::Serialize; -use std::{path::PathBuf, sync::Arc}; +use std::{io::Error, path::PathBuf, sync::Arc}; use tokio::io; use tower::util::ServiceExt; use tower_http::services::ServeDir; @@ -32,22 +32,16 @@ pub async fn serve_flists( let status = res.status(); match status { StatusCode::NOT_FOUND => { - let path = path.trim_start_matches('/'); - let path = percent_decode(path.as_ref()).decode_utf8_lossy(); - - let mut full_path = PathBuf::new(); - - // validate - for seg in path.split('/') { - if seg.starts_with("..") || seg.contains('\\') { + let full_path = match validate_path(&path) { + Ok(p) => p, + Err(_) => { return Err(ErrorTemplate { err: ResponseError::BadRequest("invalid path".to_string()), cur_path: path.to_string(), message: "invalid path".to_owned(), }); } - full_path.push(seg); - } + }; let cur_path = std::path::Path::new(&full_path); @@ -85,6 +79,23 @@ pub async fn serve_flists( }; } +fn validate_path(path: &str) -> io::Result { + let path = path.trim_start_matches('/'); + let path = percent_decode(path.as_ref()).decode_utf8_lossy(); + + let mut full_path = PathBuf::new(); + + // validate + for seg in path.split('/') { + if seg.starts_with("..") || seg.contains('\\') { + return Err(Error::other("invalid path")); + } + full_path.push(seg); + } + + Ok(full_path) +} + pub async fn visit_dir_one_level>( path: P, state: &Arc, diff --git a/frontend/.env b/frontend/.env new file mode 100644 index 0000000..415efed --- /dev/null +++ b/frontend/.env @@ -0,0 +1 @@ +VITE_API_URL="http://localhost:4000" \ No newline at end of file diff --git a/frontend/.gitignore b/frontend/.gitignore new file mode 100644 index 0000000..a547bf3 --- /dev/null +++ b/frontend/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/frontend/.vscode/extensions.json b/frontend/.vscode/extensions.json new file mode 100644 index 0000000..a7cea0b --- /dev/null +++ b/frontend/.vscode/extensions.json @@ -0,0 +1,3 @@ +{ + "recommendations": ["Vue.volar"] +} diff --git a/frontend/Dockerfile b/frontend/Dockerfile new file mode 100644 index 0000000..97380b9 --- /dev/null +++ b/frontend/Dockerfile @@ -0,0 +1,13 @@ +# build stage +FROM node:lts-alpine as build-stage +WORKDIR /app +COPY package*.json ./ +RUN npm install +COPY . . +RUN npm run build + +# production stage +FROM nginx:stable-alpine as production-stage +COPY --from=build-stage /app/dist /usr/share/nginx/html +EXPOSE 80 +CMD ["nginx", "-g", "daemon off;"] \ No newline at end of file diff --git a/frontend/README.md b/frontend/README.md new file mode 100644 index 0000000..c2154f5 --- /dev/null +++ b/frontend/README.md @@ -0,0 +1,82 @@ +# Threefold RFS + +## Description + +`Threefold RFS` is a frontend that helps manage the RFS server for creating, mounting, and extracting FungiStore lists, or fl for short. An fl is a simple format that stores information about a whole filesystem in a compact way. It doesn't hold the actual data but includes enough details to retrieve the data from a store. + +## Prerequesites + +- build essentials + + ```bash + sudo apt-get install build-essential + ``` + +- [node js](https://nodejs.org/en/download/package-manager) +- [rust](https://www.rust-lang.org/tools/install) +- Cargo, to be configured to run in the shell +- musl tool + + ```bash + sudo apt install musl-tools + ``` + +## Installation + +```bash + git clone https://github.com/threefoldtech/rfs.git +``` + +### backend + +In fl-server dir: + +- create flists dir containaing dirs for each user + ex: + - fl-server + - flists + - user1 + - user2 +- include config file + ex: + + ```yml + host='localhost' + port=4000 + store_url=['dir:///tmp/store0'] + flist_dir='flists' + + jwt_secret='secret' + jwt_expire_hours=5 + + [[users]] # list of authorized user in the server + username = "user1" + password = "password1" + + [[users]] + username = "user2" + password = "password2" + ``` + +- Move to `fl-server` directory and execute the following command to run the backend: + + ```bash + cargo run --bin fl-server -- --config-path config.toml + ``` + +### frontend + +- Move to `frontend` directory, open new terminal and execute the following commands to run the frontend: + + ```bash + npm install + npm run dev + ``` + +## Usage + +- Login with users listed in config.toml with their username and password +- Create Flist +- Preview Flist +- List all Flists +- Download Flist diff --git a/frontend/index.html b/frontend/index.html new file mode 100644 index 0000000..eb55af3 --- /dev/null +++ b/frontend/index.html @@ -0,0 +1,18 @@ + + + + + + + + Threefold Flist + + + + + +
+ + + + \ No newline at end of file diff --git a/frontend/package-lock.json b/frontend/package-lock.json new file mode 100644 index 0000000..05a018f --- /dev/null +++ b/frontend/package-lock.json @@ -0,0 +1,1476 @@ +{ + "name": "frontend", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "frontend", + "version": "0.0.0", + "dependencies": { + "@mdi/font": "^7.4.47", + "@vueuse/core": "^10.11.1", + "axios": "^1.7.3", + "filesize": "^10.1.4", + "mdi": "^2.2.43", + "vue": "^3.4.31", + "vue-router": "^4.4.2", + "vue3-toastify": "^0.2.2", + "vuetify": "^3.6.14" + }, + "devDependencies": { + "@types/node": "^22.1.0", + "@vitejs/plugin-vue": "^5.0.5", + "typescript": "^5.2.2", + "vite": "^5.3.4", + "vue-tsc": "^2.0.24" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.8.tgz", + "integrity": "sha512-pO9KhhRcuUyGnJWwyEgnRJTSIZHiT+vMD0kPeD+so0l7mxkMT19g3pjY9GTnHySck/hDzq+dtW/4VgnMkippsQ==", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.7.tgz", + "integrity": "sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w==", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.25.3", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.25.3.tgz", + "integrity": "sha512-iLTJKDbJ4hMvFPgQwwsVoxtHyWpKKPBrxkANrSYewDPaPpT5py5yeVkgPIJ7XYXhndxJpaA3PyALSXQ7u8e/Dw==", + "dependencies": { + "@babel/types": "^7.25.2" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/types": { + "version": "7.25.2", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.25.2.tgz", + "integrity": "sha512-YTnYtra7W9e6/oAZEHj0bJehPRUlLH9/fbpT5LfB0NhQXyALCRkRs3zH9v07IYhkgpqX6Z78FnuccZr/l4Fs4Q==", + "dependencies": { + "@babel/helper-string-parser": "^7.24.8", + "@babel/helper-validator-identifier": "^7.24.7", + "to-fast-properties": "^2.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==" + }, + "node_modules/@mdi/font": { + "version": "7.4.47", + "resolved": "https://registry.npmjs.org/@mdi/font/-/font-7.4.47.tgz", + "integrity": "sha512-43MtGpd585SNzHZPcYowu/84Vz2a2g31TvPMTm9uTiCSWzaheQySUcSyUH/46fPnuPQWof2yd0pGBtzee/IQWw==" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.20.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.20.0.tgz", + "integrity": "sha512-TSpWzflCc4VGAUJZlPpgAJE1+V60MePDQnBd7PPkpuEmOy8i87aL6tinFGKBFKuEDikYpig72QzdT3QPYIi+oA==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.20.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.20.0.tgz", + "integrity": "sha512-u00Ro/nok7oGzVuh/FMYfNoGqxU5CPWz1mxV85S2w9LxHR8OoMQBuSk+3BKVIDYgkpeOET5yXkx90OYFc+ytpQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.20.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.20.0.tgz", + "integrity": "sha512-uFVfvzvsdGtlSLuL0ZlvPJvl6ZmrH4CBwLGEFPe7hUmf7htGAN+aXo43R/V6LATyxlKVC/m6UsLb7jbG+LG39Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.20.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.20.0.tgz", + "integrity": "sha512-xbrMDdlev53vNXexEa6l0LffojxhqDTBeL+VUxuuIXys4x6xyvbKq5XqTXBCEUA8ty8iEJblHvFaWRJTk/icAQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.20.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.20.0.tgz", + "integrity": "sha512-jMYvxZwGmoHFBTbr12Xc6wOdc2xA5tF5F2q6t7Rcfab68TT0n+r7dgawD4qhPEvasDsVpQi+MgDzj2faOLsZjA==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.20.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.20.0.tgz", + "integrity": "sha512-1asSTl4HKuIHIB1GcdFHNNZhxAYEdqML/MW4QmPS4G0ivbEcBr1JKlFLKsIRqjSwOBkdItn3/ZDlyvZ/N6KPlw==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.20.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.20.0.tgz", + "integrity": "sha512-COBb8Bkx56KldOYJfMf6wKeYJrtJ9vEgBRAOkfw6Ens0tnmzPqvlpjZiLgkhg6cA3DGzCmLmmd319pmHvKWWlQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.20.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.20.0.tgz", + "integrity": "sha512-+it+mBSyMslVQa8wSPvBx53fYuZK/oLTu5RJoXogjk6x7Q7sz1GNRsXWjn6SwyJm8E/oMjNVwPhmNdIjwP135Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { + "version": "4.20.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.20.0.tgz", + "integrity": "sha512-yAMvqhPfGKsAxHN8I4+jE0CpLWD8cv4z7CK7BMmhjDuz606Q2tFKkWRY8bHR9JQXYcoLfopo5TTqzxgPUjUMfw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.20.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.20.0.tgz", + "integrity": "sha512-qmuxFpfmi/2SUkAw95TtNq/w/I7Gpjurx609OOOV7U4vhvUhBcftcmXwl3rqAek+ADBwSjIC4IVNLiszoj3dPA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.20.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.20.0.tgz", + "integrity": "sha512-I0BtGXddHSHjV1mqTNkgUZLnS3WtsqebAXv11D5BZE/gfw5KoyXSAXVqyJximQXNvNzUo4GKlCK/dIwXlz+jlg==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.20.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.20.0.tgz", + "integrity": "sha512-y+eoL2I3iphUg9tN9GB6ku1FA8kOfmF4oUEWhztDJ4KXJy1agk/9+pejOuZkNFhRwHAOxMsBPLbXPd6mJiCwew==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.20.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.20.0.tgz", + "integrity": "sha512-hM3nhW40kBNYUkZb/r9k2FKK+/MnKglX7UYd4ZUy5DJs8/sMsIbqWK2piZtVGE3kcXVNj3B2IrUYROJMMCikNg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.20.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.20.0.tgz", + "integrity": "sha512-psegMvP+Ik/Bg7QRJbv8w8PAytPA7Uo8fpFjXyCRHWm6Nt42L+JtoqH8eDQ5hRP7/XW2UiIriy1Z46jf0Oa1kA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.20.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.20.0.tgz", + "integrity": "sha512-GabekH3w4lgAJpVxkk7hUzUf2hICSQO0a/BLFA11/RMxQT92MabKAqyubzDZmMOC/hcJNlc+rrypzNzYl4Dx7A==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.20.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.20.0.tgz", + "integrity": "sha512-aJ1EJSuTdGnM6qbVC4B5DSmozPTqIag9fSzXRNNo+humQLG89XpPgdt16Ia56ORD7s+H8Pmyx44uczDQ0yDzpg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/estree": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", + "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==", + "dev": true + }, + "node_modules/@types/node": { + "version": "22.1.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.1.0.tgz", + "integrity": "sha512-AOmuRF0R2/5j1knA3c6G3HOk523Ga+l+ZXltX8SF1+5oqcXijjfTd8fY3XRZqSihEu9XhtQnKYLmkFaoxgsJHw==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.13.0" + } + }, + "node_modules/@types/web-bluetooth": { + "version": "0.0.20", + "resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.20.tgz", + "integrity": "sha512-g9gZnnXVq7gM7v3tJCWV/qw7w+KeOlSHAhgF9RytFyifW6AF61hdT2ucrYhPq9hLs5JIryeupHV3qGk95dH9ow==" + }, + "node_modules/@vitejs/plugin-vue": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-5.1.2.tgz", + "integrity": "sha512-nY9IwH12qeiJqumTCLJLE7IiNx7HZ39cbHaysEUd+Myvbz9KAqd2yq+U01Kab1R/H1BmiyM2ShTYlNH32Fzo3A==", + "dev": true, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "peerDependencies": { + "vite": "^5.0.0", + "vue": "^3.2.25" + } + }, + "node_modules/@volar/language-core": { + "version": "2.4.0-alpha.18", + "resolved": "https://registry.npmjs.org/@volar/language-core/-/language-core-2.4.0-alpha.18.tgz", + "integrity": "sha512-JAYeJvYQQROmVRtSBIczaPjP3DX4QW1fOqW1Ebs0d3Y3EwSNRglz03dSv0Dm61dzd0Yx3WgTW3hndDnTQqgmyg==", + "dev": true, + "dependencies": { + "@volar/source-map": "2.4.0-alpha.18" + } + }, + "node_modules/@volar/source-map": { + "version": "2.4.0-alpha.18", + "resolved": "https://registry.npmjs.org/@volar/source-map/-/source-map-2.4.0-alpha.18.tgz", + "integrity": "sha512-MTeCV9MUwwsH0sNFiZwKtFrrVZUK6p8ioZs3xFzHc2cvDXHWlYN3bChdQtwKX+FY2HG6H3CfAu1pKijolzIQ8g==", + "dev": true + }, + "node_modules/@volar/typescript": { + "version": "2.4.0-alpha.18", + "resolved": "https://registry.npmjs.org/@volar/typescript/-/typescript-2.4.0-alpha.18.tgz", + "integrity": "sha512-sXh5Y8sqGUkgxpMWUGvRXggxYHAVxg0Pa1C42lQZuPDrW6vHJPR0VCK8Sr7WJsAW530HuNQT/ZIskmXtxjybMQ==", + "dev": true, + "dependencies": { + "@volar/language-core": "2.4.0-alpha.18", + "path-browserify": "^1.0.1", + "vscode-uri": "^3.0.8" + } + }, + "node_modules/@vue/compiler-core": { + "version": "3.4.35", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.4.35.tgz", + "integrity": "sha512-gKp0zGoLnMYtw4uS/SJRRO7rsVggLjvot3mcctlMXunYNsX+aRJDqqw/lV5/gHK91nvaAAlWFgdVl020AW1Prg==", + "dependencies": { + "@babel/parser": "^7.24.7", + "@vue/shared": "3.4.35", + "entities": "^4.5.0", + "estree-walker": "^2.0.2", + "source-map-js": "^1.2.0" + } + }, + "node_modules/@vue/compiler-dom": { + "version": "3.4.35", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.4.35.tgz", + "integrity": "sha512-pWIZRL76/oE/VMhdv/ovZfmuooEni6JPG1BFe7oLk5DZRo/ImydXijoZl/4kh2406boRQ7lxTYzbZEEXEhj9NQ==", + "dependencies": { + "@vue/compiler-core": "3.4.35", + "@vue/shared": "3.4.35" + } + }, + "node_modules/@vue/compiler-sfc": { + "version": "3.4.35", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.4.35.tgz", + "integrity": "sha512-xacnRS/h/FCsjsMfxBkzjoNxyxEyKyZfBch/P4vkLRvYJwe5ChXmZZrj8Dsed/752H2Q3JE8kYu9Uyha9J6PgA==", + "dependencies": { + "@babel/parser": "^7.24.7", + "@vue/compiler-core": "3.4.35", + "@vue/compiler-dom": "3.4.35", + "@vue/compiler-ssr": "3.4.35", + "@vue/shared": "3.4.35", + "estree-walker": "^2.0.2", + "magic-string": "^0.30.10", + "postcss": "^8.4.40", + "source-map-js": "^1.2.0" + } + }, + "node_modules/@vue/compiler-ssr": { + "version": "3.4.35", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.4.35.tgz", + "integrity": "sha512-7iynB+0KB1AAJKk/biENTV5cRGHRdbdaD7Mx3nWcm1W8bVD6QmnH3B4AHhQQ1qZHhqFwzEzMwiytXm3PX1e60A==", + "dependencies": { + "@vue/compiler-dom": "3.4.35", + "@vue/shared": "3.4.35" + } + }, + "node_modules/@vue/compiler-vue2": { + "version": "2.7.16", + "resolved": "https://registry.npmjs.org/@vue/compiler-vue2/-/compiler-vue2-2.7.16.tgz", + "integrity": "sha512-qYC3Psj9S/mfu9uVi5WvNZIzq+xnXMhOwbTFKKDD7b1lhpnn71jXSFdTQ+WsIEk0ONCd7VV2IMm7ONl6tbQ86A==", + "dev": true, + "dependencies": { + "de-indent": "^1.0.2", + "he": "^1.2.0" + } + }, + "node_modules/@vue/devtools-api": { + "version": "6.6.3", + "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-6.6.3.tgz", + "integrity": "sha512-0MiMsFma/HqA6g3KLKn+AGpL1kgKhFWszC9U29NfpWK5LE7bjeXxySWJrOJ77hBz+TBrBQ7o4QJqbPbqbs8rJw==" + }, + "node_modules/@vue/language-core": { + "version": "2.0.29", + "resolved": "https://registry.npmjs.org/@vue/language-core/-/language-core-2.0.29.tgz", + "integrity": "sha512-o2qz9JPjhdoVj8D2+9bDXbaI4q2uZTHQA/dbyZT4Bj1FR9viZxDJnLcKVHfxdn6wsOzRgpqIzJEEmSSvgMvDTQ==", + "dev": true, + "dependencies": { + "@volar/language-core": "~2.4.0-alpha.18", + "@vue/compiler-dom": "^3.4.0", + "@vue/compiler-vue2": "^2.7.16", + "@vue/shared": "^3.4.0", + "computeds": "^0.0.1", + "minimatch": "^9.0.3", + "muggle-string": "^0.4.1", + "path-browserify": "^1.0.1" + }, + "peerDependencies": { + "typescript": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@vue/reactivity": { + "version": "3.4.35", + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.4.35.tgz", + "integrity": "sha512-Ggtz7ZZHakriKioveJtPlStYardwQH6VCs9V13/4qjHSQb/teE30LVJNrbBVs4+aoYGtTQKJbTe4CWGxVZrvEw==", + "dependencies": { + "@vue/shared": "3.4.35" + } + }, + "node_modules/@vue/runtime-core": { + "version": "3.4.35", + "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.4.35.tgz", + "integrity": "sha512-D+BAjFoWwT5wtITpSxwqfWZiBClhBbR+bm0VQlWYFOadUUXFo+5wbe9ErXhLvwguPiLZdEF13QAWi2vP3ZD5tA==", + "dependencies": { + "@vue/reactivity": "3.4.35", + "@vue/shared": "3.4.35" + } + }, + "node_modules/@vue/runtime-dom": { + "version": "3.4.35", + "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.4.35.tgz", + "integrity": "sha512-yGOlbos+MVhlS5NWBF2HDNgblG8e2MY3+GigHEyR/dREAluvI5tuUUgie3/9XeqhPE4LF0i2wjlduh5thnfOqw==", + "dependencies": { + "@vue/reactivity": "3.4.35", + "@vue/runtime-core": "3.4.35", + "@vue/shared": "3.4.35", + "csstype": "^3.1.3" + } + }, + "node_modules/@vue/server-renderer": { + "version": "3.4.35", + "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.4.35.tgz", + "integrity": "sha512-iZ0e/u9mRE4T8tNhlo0tbA+gzVkgv8r5BX6s1kRbOZqfpq14qoIvCZ5gIgraOmYkMYrSEZgkkojFPr+Nyq/Mnw==", + "dependencies": { + "@vue/compiler-ssr": "3.4.35", + "@vue/shared": "3.4.35" + }, + "peerDependencies": { + "vue": "3.4.35" + } + }, + "node_modules/@vue/shared": { + "version": "3.4.35", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.4.35.tgz", + "integrity": "sha512-hvuhBYYDe+b1G8KHxsQ0diDqDMA8D9laxWZhNAjE83VZb5UDaXl9Xnz7cGdDSyiHM90qqI/CyGMcpBpiDy6VVQ==" + }, + "node_modules/@vueuse/core": { + "version": "10.11.1", + "resolved": "https://registry.npmjs.org/@vueuse/core/-/core-10.11.1.tgz", + "integrity": "sha512-guoy26JQktXPcz+0n3GukWIy/JDNKti9v6VEMu6kV2sYBsWuGiTU8OWdg+ADfUbHg3/3DlqySDe7JmdHrktiww==", + "dependencies": { + "@types/web-bluetooth": "^0.0.20", + "@vueuse/metadata": "10.11.1", + "@vueuse/shared": "10.11.1", + "vue-demi": ">=0.14.8" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@vueuse/core/node_modules/vue-demi": { + "version": "0.14.10", + "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.10.tgz", + "integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==", + "hasInstallScript": true, + "bin": { + "vue-demi-fix": "bin/vue-demi-fix.js", + "vue-demi-switch": "bin/vue-demi-switch.js" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "@vue/composition-api": "^1.0.0-rc.1", + "vue": "^3.0.0-0 || ^2.6.0" + }, + "peerDependenciesMeta": { + "@vue/composition-api": { + "optional": true + } + } + }, + "node_modules/@vueuse/metadata": { + "version": "10.11.1", + "resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-10.11.1.tgz", + "integrity": "sha512-IGa5FXd003Ug1qAZmyE8wF3sJ81xGLSqTqtQ6jaVfkeZ4i5kS2mwQF61yhVqojRnenVew5PldLyRgvdl4YYuSw==", + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@vueuse/shared": { + "version": "10.11.1", + "resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-10.11.1.tgz", + "integrity": "sha512-LHpC8711VFZlDaYUXEBbFBCQ7GS3dVU9mjOhhMhXP6txTV4EhYQg/KGnQuvt/sPAtoUKq7VVUnL6mVtFoL42sA==", + "dependencies": { + "vue-demi": ">=0.14.8" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@vueuse/shared/node_modules/vue-demi": { + "version": "0.14.10", + "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.10.tgz", + "integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==", + "hasInstallScript": true, + "bin": { + "vue-demi-fix": "bin/vue-demi-fix.js", + "vue-demi-switch": "bin/vue-demi-switch.js" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "@vue/composition-api": "^1.0.0-rc.1", + "vue": "^3.0.0-0 || ^2.6.0" + }, + "peerDependenciesMeta": { + "@vue/composition-api": { + "optional": true + } + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + }, + "node_modules/axios": { + "version": "1.7.4", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.4.tgz", + "integrity": "sha512-DukmaFRnY6AzAALSH4J2M3k6PkaC+MfaAGdEERRWcC9q3/TWQwLpHR8ZRLKTdQ3aBDL64EdluRDjJqKw+BPZEw==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/computeds": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/computeds/-/computeds-0.0.1.tgz", + "integrity": "sha512-7CEBgcMjVmitjYo5q8JTJVra6X5mQ20uTThdK+0kR7UEaDrAWEQcRiBtWJzga4eRpP6afNwwLsX2SET2JhVB1Q==", + "dev": true + }, + "node_modules/csstype": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==" + }, + "node_modules/de-indent": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/de-indent/-/de-indent-1.0.2.tgz", + "integrity": "sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==", + "dev": true + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==" + }, + "node_modules/filesize": { + "version": "10.1.4", + "resolved": "https://registry.npmjs.org/filesize/-/filesize-10.1.4.tgz", + "integrity": "sha512-ryBwPIIeErmxgPnm6cbESAzXjuEFubs+yKYLBZvg3CaiNcmkJChoOGcBSrZ6IwkMwPABwPpVXE6IlNdGJJrvEg==", + "license": "BSD-3-Clause", + "engines": { + "node": ">= 10.4.0" + } + }, + "node_modules/follow-redirects": { + "version": "1.15.6", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz", + "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "dev": true, + "bin": { + "he": "bin/he" + } + }, + "node_modules/magic-string": { + "version": "0.30.11", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.11.tgz", + "integrity": "sha512-+Wri9p0QHMy+545hKww7YAu5NyzF8iomPL/RQazugQ9+Ez4Ic3mERMd8ZTX5rfK944j+560ZJi8iAwgak1Ac7A==", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0" + } + }, + "node_modules/mdi": { + "version": "2.2.43", + "resolved": "https://registry.npmjs.org/mdi/-/mdi-2.2.43.tgz", + "integrity": "sha512-g3m6z4303qieltUM20JL2gdsJZvoVzIzO74qa2XxZ2kg9JPwrPEAgooVhRDHZi1vvRh0gB8Dg+c9XqNdz4jcIg==", + "deprecated": "The mdi package was renamed to @mdi/font after v2.2.43. Please rename in your package.json for future updates." + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/muggle-string": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/muggle-string/-/muggle-string-0.4.1.tgz", + "integrity": "sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==", + "dev": true + }, + "node_modules/nanoid": { + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", + "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/path-browserify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", + "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==", + "dev": true + }, + "node_modules/picocolors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz", + "integrity": "sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==" + }, + "node_modules/postcss": { + "version": "8.4.40", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.40.tgz", + "integrity": "sha512-YF2kKIUzAofPMpfH6hOi2cGnv/HrUlfucspc7pDyvv7kGdqXrfj8SCl/t8owkEgKEuu8ZcRjSOxFxVLqwChZ2Q==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "nanoid": "^3.3.7", + "picocolors": "^1.0.1", + "source-map-js": "^1.2.0" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + }, + "node_modules/rollup": { + "version": "4.20.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.20.0.tgz", + "integrity": "sha512-6rbWBChcnSGzIlXeIdNIZTopKYad8ZG8ajhl78lGRLsI2rX8IkaotQhVas2Ma+GPxJav19wrSzvRvuiv0YKzWw==", + "dev": true, + "dependencies": { + "@types/estree": "1.0.5" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.20.0", + "@rollup/rollup-android-arm64": "4.20.0", + "@rollup/rollup-darwin-arm64": "4.20.0", + "@rollup/rollup-darwin-x64": "4.20.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.20.0", + "@rollup/rollup-linux-arm-musleabihf": "4.20.0", + "@rollup/rollup-linux-arm64-gnu": "4.20.0", + "@rollup/rollup-linux-arm64-musl": "4.20.0", + "@rollup/rollup-linux-powerpc64le-gnu": "4.20.0", + "@rollup/rollup-linux-riscv64-gnu": "4.20.0", + "@rollup/rollup-linux-s390x-gnu": "4.20.0", + "@rollup/rollup-linux-x64-gnu": "4.20.0", + "@rollup/rollup-linux-x64-musl": "4.20.0", + "@rollup/rollup-win32-arm64-msvc": "4.20.0", + "@rollup/rollup-win32-ia32-msvc": "4.20.0", + "@rollup/rollup-win32-x64-msvc": "4.20.0", + "fsevents": "~2.3.2" + } + }, + "node_modules/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/source-map-js": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz", + "integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/to-fast-properties": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", + "engines": { + "node": ">=4" + } + }, + "node_modules/typescript": { + "version": "5.5.4", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.4.tgz", + "integrity": "sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==", + "devOptional": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.13.0.tgz", + "integrity": "sha512-xtFJHudx8S2DSoujjMd1WeWvn7KKWFRESZTMeL1RptAYERu29D6jphMjjY+vn96jvN3kVPDNxU/E13VTaXj6jg==", + "dev": true, + "license": "MIT" + }, + "node_modules/vite": { + "version": "5.3.5", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.3.5.tgz", + "integrity": "sha512-MdjglKR6AQXQb9JGiS7Rc2wC6uMjcm7Go/NHNO63EwiJXfuk9PgqiP/n5IDJCziMkfw9n4Ubp7lttNwz+8ZVKA==", + "dev": true, + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.39", + "rollup": "^4.13.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/vscode-uri": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.0.8.tgz", + "integrity": "sha512-AyFQ0EVmsOZOlAnxoFOGOq1SQDWAB7C6aqMGS23svWAllfOaxbuFvcT8D1i8z3Gyn8fraVeZNNmN6e9bxxXkKw==", + "dev": true + }, + "node_modules/vue": { + "version": "3.4.35", + "resolved": "https://registry.npmjs.org/vue/-/vue-3.4.35.tgz", + "integrity": "sha512-+fl/GLmI4GPileHftVlCdB7fUL4aziPcqTudpTGXCT8s+iZWuOCeNEB5haX6Uz2IpRrbEXOgIFbe+XciCuGbNQ==", + "dependencies": { + "@vue/compiler-dom": "3.4.35", + "@vue/compiler-sfc": "3.4.35", + "@vue/runtime-dom": "3.4.35", + "@vue/server-renderer": "3.4.35", + "@vue/shared": "3.4.35" + }, + "peerDependencies": { + "typescript": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/vue-router": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.4.2.tgz", + "integrity": "sha512-1qNybkn2L7QsLzaXs8nvlQmRKp8XF8DCxZys/Jr1JpQcHsKUxTKzTxCVA1G7NfBfwRIBgCJPoujOG5lHCCNUxw==", + "dependencies": { + "@vue/devtools-api": "^6.6.3" + }, + "funding": { + "url": "https://github.com/sponsors/posva" + }, + "peerDependencies": { + "vue": "^3.2.0" + } + }, + "node_modules/vue-tsc": { + "version": "2.0.29", + "resolved": "https://registry.npmjs.org/vue-tsc/-/vue-tsc-2.0.29.tgz", + "integrity": "sha512-MHhsfyxO3mYShZCGYNziSbc63x7cQ5g9kvijV7dRe1TTXBRLxXyL0FnXWpUF1xII2mJ86mwYpYsUmMwkmerq7Q==", + "dev": true, + "dependencies": { + "@volar/typescript": "~2.4.0-alpha.18", + "@vue/language-core": "2.0.29", + "semver": "^7.5.4" + }, + "bin": { + "vue-tsc": "bin/vue-tsc.js" + }, + "peerDependencies": { + "typescript": ">=5.0.0" + } + }, + "node_modules/vue3-toastify": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/vue3-toastify/-/vue3-toastify-0.2.2.tgz", + "integrity": "sha512-D8pmIp2UeU8MU1OY7GktA70HviZ38b1RagN82P7tFu3abUD86w+PjfmbdRch4QVtjVxK+eqKLvi5cXJRndwJfw==", + "engines": { + "node": ">=18.18.0", + "npm": ">=9.0.0" + }, + "peerDependencies": { + "vue": ">=3.2.0" + }, + "peerDependenciesMeta": { + "vue": { + "optional": true + } + } + }, + "node_modules/vuetify": { + "version": "3.6.14", + "resolved": "https://registry.npmjs.org/vuetify/-/vuetify-3.6.14.tgz", + "integrity": "sha512-iSa3CgdTEt/7B0aGDmkBARe8rxDDycEYHu1zNtOf1Xpvs/Tv7Ql5yHGqM2XCY0h7SL8Dme39pJIovzg3q4JLbQ==", + "engines": { + "node": "^12.20 || >=14.13" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/johnleider" + }, + "peerDependencies": { + "typescript": ">=4.7", + "vite-plugin-vuetify": ">=1.0.0", + "vue": "^3.3.0", + "vue-i18n": "^9.0.0", + "webpack-plugin-vuetify": ">=2.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + }, + "vite-plugin-vuetify": { + "optional": true + }, + "vue-i18n": { + "optional": true + }, + "webpack-plugin-vuetify": { + "optional": true + } + } + } + } +} diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 0000000..8ff5dbb --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,29 @@ +{ + "name": "frontend", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vue-tsc -b && vite build", + "preview": "vite preview" + }, + "dependencies": { + "@mdi/font": "^7.4.47", + "@vueuse/core": "^10.11.1", + "axios": "^1.7.3", + "filesize": "^10.1.4", + "mdi": "^2.2.43", + "vue": "^3.4.31", + "vue-router": "^4.4.2", + "vue3-toastify": "^0.2.2", + "vuetify": "^3.6.14" + }, + "devDependencies": { + "@types/node": "^22.1.0", + "@vitejs/plugin-vue": "^5.0.5", + "typescript": "^5.2.2", + "vite": "^5.3.4", + "vue-tsc": "^2.0.24" + } +} diff --git a/frontend/public/vite.svg b/frontend/public/vite.svg new file mode 100644 index 0000000..e7b8dfb --- /dev/null +++ b/frontend/public/vite.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/src/App.vue b/frontend/src/App.vue new file mode 100644 index 0000000..97f1b9b --- /dev/null +++ b/frontend/src/App.vue @@ -0,0 +1,33 @@ + + + + + diff --git a/frontend/src/assets/Image.png b/frontend/src/assets/Image.png new file mode 100644 index 0000000..2602b86 Binary files /dev/null and b/frontend/src/assets/Image.png differ diff --git a/frontend/src/assets/Image.png:Zone.Identifier b/frontend/src/assets/Image.png:Zone.Identifier new file mode 100644 index 0000000..e69de29 diff --git a/frontend/src/assets/home.png b/frontend/src/assets/home.png new file mode 100644 index 0000000..6cc1ca9 Binary files /dev/null and b/frontend/src/assets/home.png differ diff --git a/frontend/src/assets/home.png:Zone.Identifier b/frontend/src/assets/home.png:Zone.Identifier new file mode 100644 index 0000000..e69de29 diff --git a/frontend/src/assets/logo.png b/frontend/src/assets/logo.png new file mode 100644 index 0000000..420eb7b Binary files /dev/null and b/frontend/src/assets/logo.png differ diff --git a/frontend/src/assets/logo.png:Zone.Identifier b/frontend/src/assets/logo.png:Zone.Identifier new file mode 100644 index 0000000..e69de29 diff --git a/frontend/src/assets/logo_white.png b/frontend/src/assets/logo_white.png new file mode 100644 index 0000000..319d3ab Binary files /dev/null and b/frontend/src/assets/logo_white.png differ diff --git a/frontend/src/assets/logo_white.png:Zone.Identifier b/frontend/src/assets/logo_white.png:Zone.Identifier new file mode 100644 index 0000000..e69de29 diff --git a/frontend/src/assets/side.png b/frontend/src/assets/side.png new file mode 100644 index 0000000..aad374f Binary files /dev/null and b/frontend/src/assets/side.png differ diff --git a/frontend/src/assets/side.png:Zone.Identifier b/frontend/src/assets/side.png:Zone.Identifier new file mode 100644 index 0000000..e69de29 diff --git a/frontend/src/client.ts b/frontend/src/client.ts new file mode 100644 index 0000000..d4b88ad --- /dev/null +++ b/frontend/src/client.ts @@ -0,0 +1,11 @@ +import axios from "axios"; + + + +export const api = axios.create({ + baseURL: import.meta.env.VITE_API_URL, + headers: { + "Content-Type": "application/json", + Authorization: "Bearer " + sessionStorage.getItem("token"), + }, +}); diff --git a/frontend/src/components/CreateFlist.vue b/frontend/src/components/CreateFlist.vue new file mode 100644 index 0000000..18a45ef --- /dev/null +++ b/frontend/src/components/CreateFlist.vue @@ -0,0 +1,308 @@ + + + + diff --git a/frontend/src/components/Footer.vue b/frontend/src/components/Footer.vue new file mode 100644 index 0000000..eb3e478 --- /dev/null +++ b/frontend/src/components/Footer.vue @@ -0,0 +1,23 @@ + + + \ No newline at end of file diff --git a/frontend/src/components/Home.vue b/frontend/src/components/Home.vue new file mode 100644 index 0000000..c215b7e --- /dev/null +++ b/frontend/src/components/Home.vue @@ -0,0 +1,158 @@ + + + + diff --git a/frontend/src/components/Login.vue b/frontend/src/components/Login.vue new file mode 100644 index 0000000..f316704 --- /dev/null +++ b/frontend/src/components/Login.vue @@ -0,0 +1,113 @@ + + + diff --git a/frontend/src/components/Navbar.vue b/frontend/src/components/Navbar.vue new file mode 100644 index 0000000..18cc9b9 --- /dev/null +++ b/frontend/src/components/Navbar.vue @@ -0,0 +1,63 @@ + + + diff --git a/frontend/src/components/PreviewFlist.vue b/frontend/src/components/PreviewFlist.vue new file mode 100644 index 0000000..8ace5f4 --- /dev/null +++ b/frontend/src/components/PreviewFlist.vue @@ -0,0 +1,140 @@ + + + + + \ No newline at end of file diff --git a/frontend/src/components/UserFlist.vue b/frontend/src/components/UserFlist.vue new file mode 100644 index 0000000..6b33555 --- /dev/null +++ b/frontend/src/components/UserFlist.vue @@ -0,0 +1,129 @@ + + + + diff --git a/frontend/src/helpers.ts b/frontend/src/helpers.ts new file mode 100644 index 0000000..a59164e --- /dev/null +++ b/frontend/src/helpers.ts @@ -0,0 +1,8 @@ +import { useClipboard } from "@vueuse/core"; +import { toast } from "vue3-toastify"; +const { copy } = useClipboard(); + +export const copyLink = (url: string) => { + copy(url); + toast.success("Link Copied to Clipboard"); +}; diff --git a/frontend/src/main.ts b/frontend/src/main.ts new file mode 100644 index 0000000..bde59ba --- /dev/null +++ b/frontend/src/main.ts @@ -0,0 +1,17 @@ +import { createApp } from "vue"; +import "vuetify/styles"; +import { createVuetify } from "vuetify"; +import * as components from "vuetify/components"; +import * as directives from "vuetify/directives"; +import App from "./App.vue"; +import router from "./router/index"; +import createToast from "vue3-toastify"; + +const toast = createToast; + +const vuetify = createVuetify({ + components, + directives, +}); + +createApp(App).use(router).use(toast).use(vuetify).mount("#app"); diff --git a/frontend/src/router/index.ts b/frontend/src/router/index.ts new file mode 100644 index 0000000..48d9d4f --- /dev/null +++ b/frontend/src/router/index.ts @@ -0,0 +1,52 @@ +import { createRouter, createWebHistory, RouteRecordRaw } from "vue-router"; +const Login = () => import("../components/Login.vue"); +const CreateFlist = () => import("../components/CreateFlist.vue"); +const Home = () => import("../components/Home.vue"); +const UserFlist = () => import("../components/UserFlist.vue"); +const PreviewFlist = () => import("../components/PreviewFlist.vue"); + +const routes: Array = [ + { + path: "/login", + name: "login", + component: Login, + }, + { + path: "/myflists", + name: "myflists", + component: UserFlist, + meta: { requiresAuth: true }, + }, + { + path: "/create", + name: "create", + component: CreateFlist, + meta: { requiresAuth: true }, + }, + { + path: "/flists/:username/:id", + name: "previewflist", + component: PreviewFlist, + }, + { + path: "/", + name: "home", + component: Home, + }, +]; + +const router = createRouter({ + history: createWebHistory(import.meta.env.BASE_URL), + routes, +}); + +router.beforeEach((to, _, next) => { + const token: string | null = sessionStorage.getItem("token"); + if (to.meta.requiresAuth && (token == null || token.length == 0)) { + next({ name: "login" }); + } else { + next(); + } +}); + +export default router; diff --git a/frontend/src/style.css b/frontend/src/style.css new file mode 100644 index 0000000..b3ff56e --- /dev/null +++ b/frontend/src/style.css @@ -0,0 +1,21 @@ +.background-green { + background-color: #1aa18f !important; +} +.thick-border .v-data-table__wrapper { + border: 3px solid #000; +} +.v-data-table-footer__items-per-page { + display: none !important; +} +.v-data-table td{ + padding: 4px 8px; + font-size: 12px; + font-weight: 500; +} +.mn-height { + min-height: calc(100% - 7%); +} + +.file-name { + font-weight: 500; +} \ No newline at end of file diff --git a/frontend/src/types/Flist.ts b/frontend/src/types/Flist.ts new file mode 100644 index 0000000..bd354f2 --- /dev/null +++ b/frontend/src/types/Flist.ts @@ -0,0 +1,30 @@ +export interface Flist { + auth: string; + email: string; + identity_token: string; + image_name: string; + password: string; + registry_token: string; + server_address: string; + username: string; +} + + +export interface FlistBody { + is_file: Boolean; + last_modified: bigint; + name: string; + path_uri: string; + progress: number; + size: number; +} + +export interface FlistsResponseInterface { + [key: string]: FlistBody[]; +} + +export interface FlistPreview{ + checksum: string; + content: string[]; + metadata: string; +} diff --git a/frontend/src/types/User.ts b/frontend/src/types/User.ts new file mode 100644 index 0000000..307a12c --- /dev/null +++ b/frontend/src/types/User.ts @@ -0,0 +1,4 @@ +export interface User { + username: string; + password: string; +} diff --git a/frontend/src/vite-env.d.ts b/frontend/src/vite-env.d.ts new file mode 100644 index 0000000..11f02fe --- /dev/null +++ b/frontend/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/frontend/tsconfig.app.json b/frontend/tsconfig.app.json new file mode 100644 index 0000000..3286945 --- /dev/null +++ b/frontend/tsconfig.app.json @@ -0,0 +1,27 @@ +{ + "compilerOptions": { + "composite": true, + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", + "target": "ES2021", + "useDefineForClassFields": true, + "module": "ESNext", + "lib": ["ES2021", "DOM", "DOM.Iterable"], + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "moduleDetection": "force", + "noEmit": true, + "jsx": "preserve", + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"] +} diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json new file mode 100644 index 0000000..ea9d0cd --- /dev/null +++ b/frontend/tsconfig.json @@ -0,0 +1,11 @@ +{ + "files": [], + "references": [ + { + "path": "./tsconfig.app.json" + }, + { + "path": "./tsconfig.node.json" + } + ] +} diff --git a/frontend/tsconfig.node.json b/frontend/tsconfig.node.json new file mode 100644 index 0000000..3afdd6e --- /dev/null +++ b/frontend/tsconfig.node.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "composite": true, + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", + "skipLibCheck": true, + "module": "ESNext", + "moduleResolution": "bundler", + "allowSyntheticDefaultImports": true, + "strict": true, + "noEmit": true + }, + "include": ["vite.config.ts"] +} diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts new file mode 100644 index 0000000..05c1740 --- /dev/null +++ b/frontend/vite.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from 'vite' +import vue from '@vitejs/plugin-vue' + +// https://vitejs.dev/config/ +export default defineConfig({ + plugins: [vue()], +}) diff --git a/rfs/Cargo.toml b/rfs/Cargo.toml index 7ea2507..8081720 100644 --- a/rfs/Cargo.toml +++ b/rfs/Cargo.toml @@ -55,6 +55,7 @@ rust-s3 = "0.34.0-rc3" openssl = { version = "0.10", features = ["vendored"] } regex = "1.9.6" which = "6.0" +reqwest = "0.11" [dependencies.polyfuse] branch = "master" diff --git a/rfs/README.md b/rfs/README.md index 45f529e..048190f 100644 --- a/rfs/README.md +++ b/rfs/README.md @@ -22,7 +22,7 @@ to be able to use from anywhere on your system. ## Stores -A store in where the actual data lives. A store can be as simple as a `directory` on your local machine in that case the files on the `fl` are only 'accessible' on your local machine. A store can also be a `zdb` running remotely or a cluster of `zdb`. Right now only `dir`, `zdb` and `s3` stores are supported but this will change in the future to support even more stores. +A store in where the actual data lives. A store can be as simple as a `directory` on your local machine in that case the files on the `fl` are only 'accessible' on your local machine. A store can also be a `zdb` running remotely or a cluster of `zdb`. Right now only `dir`, `http`, `zdb` and `s3` stores are supported but this will change in the future to support even more stores. ## Usage @@ -41,6 +41,8 @@ The simplest form of `` is a `url`. the store `url` defines the sto - `s3`: aws-s3 is used for storing and retrieving large amounts of data (blobs) in buckets (directories). An example `s3://:@:/` `region` is an optional param for s3 stores, if you want to provide one you can add it as a query to the url `?region=` +- `http`: http is a store mostly used for wrapping a dir store to fetch data through http requests. It does not support uploading, just fetching the data. + It can be set in the FL file as the store to fetch the data with `rfs config`. Example: `http://localhost:9000/store` (https works too). `` can also be of the form `-=` where `start` and `end` are a hex bytes for partitioning of blob keys. rfs will then store a set of blobs on the defined store if they blob key falls in the `[start:end]` range (inclusive). @@ -48,7 +50,7 @@ If the `start-end` range is not provided a `00-FF` range is assume basically a c This is only useful because `rfs` can accept multiple stores on the command line with different and/or overlapping ranges. -For example `-s 00-80=dir:///tmp/store0 -s 81-ff=dir://tmp/store1` means all keys that has prefix byte in range `[00-80]` will be written to /tmp/store0 all other keys `00-ff` will be written to store1. +For example `-s 00-80=dir:///tmp/store0 -s 81-ff=dir:///tmp/store1` means all keys that has prefix byte in range `[00-80]` will be written to /tmp/store0 all other keys `[81-ff]` will be written to store1. The same range can appear multiple times, which means the blob will be replicated to all the stores that matches its key prefix. diff --git a/rfs/src/clone.rs b/rfs/src/clone.rs new file mode 100644 index 0000000..6ddfcbe --- /dev/null +++ b/rfs/src/clone.rs @@ -0,0 +1,128 @@ +use crate::{ + cache::Cache, + fungi::{meta::Block, Reader, Result}, + store::{BlockStore, Store}, +}; +use anyhow::Error; +use futures::lock::Mutex; +use hex::ToHex; +use std::sync::Arc; +use tokio::io::AsyncReadExt; + +const WORKERS: usize = 10; + +pub async fn clone(reader: Reader, store: S, cache: Cache) -> Result<()> { + let failures = Arc::new(Mutex::new(Vec::new())); + let cloner = BlobCloner::new(cache, store.into(), failures.clone()); + let mut workers = workers::WorkerPool::new(cloner, WORKERS); + + let mut offset = 0; + loop { + if !failures.lock().await.is_empty() { + break; + } + let blocks = reader.all_blocks(1000, offset).await?; + if blocks.is_empty() { + break; + } + for block in blocks { + offset += 1; + let worker = workers.get().await; + worker.send(block)?; + } + } + + workers.close().await; + let failures = failures.lock().await; + + if failures.is_empty() { + return Ok(()); + } + + log::error!("failed to clone one or more blocks"); + for (block, error) in failures.iter() { + log::error!(" - failed to clone block {}: {}", block, error); + } + + Err(crate::fungi::Error::Anyhow(anyhow::anyhow!( + "failed to clone ({}) blocks", + failures.len() + ))) +} + +struct BlobCloner +where + S: Store, +{ + cache: Arc>, + store: Arc>, + failures: Arc>>, +} + +impl Clone for BlobCloner +where + S: Store, +{ + fn clone(&self) -> Self { + Self { + cache: self.cache.clone(), + store: self.store.clone(), + failures: self.failures.clone(), + } + } +} + +impl BlobCloner +where + S: Store, +{ + fn new( + cache: Cache, + store: BlockStore, + failures: Arc>>, + ) -> Self { + Self { + cache: Arc::new(cache), + store: Arc::new(store), + failures, + } + } +} + +#[async_trait::async_trait] +impl workers::Work for BlobCloner +where + S: Store, +{ + type Input = Block; + type Output = (); + + async fn run(&mut self, block: Self::Input) -> Self::Output { + let mut file = match self.cache.get(&block).await { + Ok((_, f)) => f, + Err(err) => { + self.failures + .lock() + .await + .push((block.id.as_slice().encode_hex(), err)); + return; + } + }; + + let mut data = Vec::new(); + if let Err(err) = file.read_to_end(&mut data).await { + self.failures + .lock() + .await + .push((block.id.as_slice().encode_hex(), err.into())); + return; + } + if let Err(err) = self.store.set(&data).await { + self.failures + .lock() + .await + .push((block.id.as_slice().encode_hex(), err.into())); + return; + } + } +} diff --git a/rfs/src/config.rs b/rfs/src/config.rs new file mode 100644 index 0000000..62eaf4d --- /dev/null +++ b/rfs/src/config.rs @@ -0,0 +1,72 @@ +use crate::{ + fungi::{meta::Tag, Reader, Result, Writer}, + store::{self, Store}, +}; + +pub async fn tag_list(reader: Reader) -> Result<()> { + let tags = reader.tags().await?; + if !tags.is_empty() { + println!("tags:"); + } + for (key, value) in tags { + println!("\t{}={}", key, value); + } + Ok(()) +} + +pub async fn tag_add(writer: Writer, tags: Vec<(String, String)>) -> Result<()> { + for (key, value) in tags { + writer.tag(Tag::Custom(key.as_str()), value).await?; + } + Ok(()) +} + +pub async fn tag_delete(writer: Writer, keys: Vec, all: bool) -> Result<()> { + if all { + writer.delete_tags().await?; + return Ok(()); + } + for key in keys { + writer.delete_tag(Tag::Custom(key.as_str())).await?; + } + Ok(()) +} + +pub async fn store_list(reader: Reader) -> Result<()> { + let routes = reader.routes().await?; + if !routes.is_empty() { + println!("routes:") + } + for route in routes { + println!( + "\trange:[{}-{}] store:{}", + route.start, route.end, route.url + ); + } + Ok(()) +} + +pub async fn store_add(writer: Writer, stores: Vec) -> Result<()> { + let store = store::parse_router(stores.as_slice()).await?; + for route in store.routes() { + writer + .route( + route.start.unwrap_or(u8::MIN), + route.end.unwrap_or(u8::MAX), + route.url, + ) + .await?; + } + Ok(()) +} + +pub async fn store_delete(writer: Writer, stores: Vec, all: bool) -> Result<()> { + if all { + writer.delete_routes().await?; + return Ok(()); + } + for store in stores { + writer.delete_route(store).await?; + } + Ok(()) +} diff --git a/rfs/src/fungi/meta.rs b/rfs/src/fungi/meta.rs index 8e13789..211f947 100644 --- a/rfs/src/fungi/meta.rs +++ b/rfs/src/fungi/meta.rs @@ -268,6 +268,16 @@ impl Reader { Ok(results) } + pub async fn all_blocks(&self, limit: u32, offset: u64) -> Result> { + let results: Vec = sqlx::query_as("select id, key from block limit ? offset ?;") + .bind(limit) + .bind(offset as i64) + .fetch_all(&self.pool) + .await?; + + Ok(results) + } + pub async fn tag(&self, tag: Tag<'_>) -> Result> { let value: Option<(String,)> = sqlx::query_as("select value from tag where key = ?;") .bind(tag.key()) @@ -277,6 +287,14 @@ impl Reader { Ok(value.map(|v| v.0)) } + pub async fn tags(&self) -> Result> { + let tags: Vec<(String, String)> = sqlx::query_as("select key, value from tag;") + .fetch_all(&self.pool) + .await?; + + Ok(tags) + } + pub async fn routes(&self) -> Result> { let results: Vec = sqlx::query_as("select start, end, url from route;") .fetch_all(&self.pool) @@ -340,8 +358,10 @@ pub struct Writer { impl Writer { /// create a new mkondo writer - pub async fn new>(path: P) -> Result { - let _ = tokio::fs::remove_file(&path).await; + pub async fn new>(path: P, remove: bool) -> Result { + if remove { + let _ = tokio::fs::remove_file(&path).await; + } let opts = SqliteConnectOptions::new() .create_if_missing(true) @@ -409,13 +429,39 @@ impl Writer { } pub async fn tag>(&self, tag: Tag<'_>, value: V) -> Result<()> { - sqlx::query("insert into tag (key, value) values (?, ?);") + sqlx::query("insert or replace into tag (key, value) values (?, ?);") .bind(tag.key()) .bind(value.as_ref()) .execute(&self.pool) .await?; Ok(()) } + pub async fn delete_tag(&self, tag: Tag<'_>) -> Result<()> { + sqlx::query("delete from tag where key = ?;") + .bind(tag.key()) + .execute(&self.pool) + .await?; + Ok(()) + } + + pub async fn delete_route>(&self, url: U) -> Result<()> { + sqlx::query("delete from route where url = ?;") + .bind(url.as_ref()) + .execute(&self.pool) + .await?; + Ok(()) + } + + pub async fn delete_tags(&self) -> Result<()> { + sqlx::query("delete from tag;").execute(&self.pool).await?; + Ok(()) + } + pub async fn delete_routes(&self) -> Result<()> { + sqlx::query("delete from route;") + .execute(&self.pool) + .await?; + Ok(()) + } } #[cfg(test)] @@ -425,7 +471,7 @@ mod test { #[tokio::test] async fn test_inode() { const PATH: &str = "/tmp/inode.fl"; - let meta = Writer::new(PATH).await.unwrap(); + let meta = Writer::new(PATH, true).await.unwrap(); let ino = meta .inode(Inode { @@ -449,7 +495,7 @@ mod test { #[tokio::test] async fn test_get_children() { const PATH: &str = "/tmp/children.fl"; - let meta = Writer::new(PATH).await.unwrap(); + let meta = Writer::new(PATH, true).await.unwrap(); let ino = meta .inode(Inode { @@ -486,7 +532,7 @@ mod test { #[tokio::test] async fn test_get_block() { const PATH: &str = "/tmp/block.fl"; - let meta = Writer::new(PATH).await.unwrap(); + let meta = Writer::new(PATH, true).await.unwrap(); let hash: [u8; ID_LEN] = [ 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, @@ -509,7 +555,7 @@ mod test { #[tokio::test] async fn test_get_tag() { const PATH: &str = "/tmp/tag.fl"; - let meta = Writer::new(PATH).await.unwrap(); + let meta = Writer::new(PATH, true).await.unwrap(); meta.tag(Tag::Version, "0.1").await.unwrap(); meta.tag(Tag::Author, "azmy").await.unwrap(); meta.tag(Tag::Custom("custom"), "value").await.unwrap(); @@ -535,7 +581,7 @@ mod test { #[tokio::test] async fn test_get_routes() { const PATH: &str = "/tmp/route.fl"; - let meta = Writer::new(PATH).await.unwrap(); + let meta = Writer::new(PATH, true).await.unwrap(); meta.route(0, 128, "zdb://hub1.grid.tf").await.unwrap(); meta.route(129, 255, "zdb://hub2.grid.tf").await.unwrap(); @@ -560,7 +606,7 @@ mod test { #[tokio::test] async fn test_walk() { const PATH: &str = "/tmp/walk.fl"; - let meta = Writer::new(PATH).await.unwrap(); + let meta = Writer::new(PATH, true).await.unwrap(); let parent = meta .inode(Inode { diff --git a/rfs/src/lib.rs b/rfs/src/lib.rs index d85fe32..a3299d8 100644 --- a/rfs/src/lib.rs +++ b/rfs/src/lib.rs @@ -9,6 +9,9 @@ mod pack; pub use pack::pack; mod unpack; pub use unpack::unpack; +mod clone; +pub use clone::clone; +pub mod config; const PARALLEL_UPLOAD: usize = 10; // number of files we can upload in parallel @@ -53,7 +56,7 @@ mod test { } println!("file generation complete"); - let writer = meta::Writer::new(root.join("meta.fl")).await.unwrap(); + let writer = meta::Writer::new(root.join("meta.fl"), true).await.unwrap(); // while we at it we can already create 2 stores and create a router store on top // of that. diff --git a/rfs/src/main.rs b/rfs/src/main.rs index e840964..7e57355 100644 --- a/rfs/src/main.rs +++ b/rfs/src/main.rs @@ -2,14 +2,15 @@ extern crate log; use nix::sys::signal::{self, Signal}; use nix::unistd::Pid; +use std::error::Error; use std::io::Read; use anyhow::{Context, Result}; use clap::{ArgAction, Args, Parser, Subcommand}; -use rfs::cache; use rfs::fungi; use rfs::store::{self, Router, Stores}; +use rfs::{cache, config}; mod fs; /// mount flists @@ -32,6 +33,10 @@ enum Commands { Pack(PackOptions), /// unpack (downloads) content of an FL the provided location Unpack(UnpackOptions), + /// clone copies the data from the stores of an FL to another stores + Clone(CloneOptions), + /// list or modify FL metadata and stores + Config(ConfigOptions), } #[derive(Args, Debug)] @@ -90,10 +95,107 @@ struct UnpackOptions { #[clap(short, long, default_value_t = false)] preserve_ownership: bool, - /// target directory to upload + /// target directory for unpacking target: String, } +#[derive(Args, Debug)] +struct CloneOptions { + /// path to metadata file (flist) + #[clap(short, long)] + meta: String, + + /// store url in the format [xx-xx=]. the range xx-xx is optional and used for + /// sharding. the URL is per store type, please check docs for more information + #[clap(short, long, action=ArgAction::Append)] + store: Vec, + + /// directory used as cache for downloaded file chunks + #[clap(short, long, default_value_t = String::from("/tmp/cache"))] + cache: String, +} + +#[derive(Args, Debug)] +struct ConfigOptions { + /// path to metadata file (flist) + #[clap(short, long)] + meta: String, + + #[command(subcommand)] + command: ConfigCommands, +} + +#[derive(Subcommand, Debug)] +enum ConfigCommands { + #[command(subcommand)] + Tag(TagOperation), + #[command(subcommand)] + Store(StoreOperation), +} + +#[derive(Subcommand, Debug)] +enum TagOperation { + List, + Add(TagAddOptions), + Delete(TagDeleteOptions), +} + +#[derive(Args, Debug)] +struct TagAddOptions { + /// pair of key-values separated with '=' + #[clap(short, long, value_parser = parse_key_val::, number_of_values = 1)] + tag: Vec<(String, String)>, +} + +#[derive(Args, Debug)] +struct TagDeleteOptions { + /// key to remove + #[clap(short, long, action=ArgAction::Append)] + key: Vec, + /// remove all tags + #[clap(short, long, default_value_t = false)] + all: bool, +} + +#[derive(Subcommand, Debug)] +enum StoreOperation { + List, + Add(StoreAddOptions), + Delete(StoreDeleteOptions), +} + +#[derive(Args, Debug)] +struct StoreAddOptions { + /// store url in the format [xx-xx=]. the range xx-xx is optional and used for + /// sharding. the URL is per store type, please check docs for more information + #[clap(short, long, action=ArgAction::Append)] + store: Vec, +} + +#[derive(Args, Debug)] +struct StoreDeleteOptions { + /// store to remove + #[clap(short, long, action=ArgAction::Append)] + store: Vec, + /// remove all stores + #[clap(short, long, default_value_t = false)] + all: bool, +} + +/// Parse a single key-value pair +fn parse_key_val(s: &str) -> Result<(T, U), Box> +where + T: std::str::FromStr, + T::Err: Error + Send + Sync + 'static, + U: std::str::FromStr, + U::Err: Error + Send + Sync + 'static, +{ + let pos = s + .find('=') + .ok_or_else(|| format!("invalid KEY=value: no `=` found in `{s}`"))?; + Ok((s[..pos].parse()?, s[pos + 1..].parse()?)) +} + fn main() -> Result<()> { let opts = Options::parse(); @@ -115,6 +217,8 @@ fn main() -> Result<()> { Commands::Mount(opts) => mount(opts), Commands::Pack(opts) => pack(opts), Commands::Unpack(opts) => unpack(opts), + Commands::Clone(opts) => clone(opts), + Commands::Config(opts) => config(opts), } } @@ -123,8 +227,8 @@ fn pack(opts: PackOptions) -> Result<()> { rt.block_on(async move { let store = store::parse_router(opts.store.as_slice()).await?; - let meta = fungi::Writer::new(opts.meta).await?; - rfs::pack(meta, store, opts.target, !opts.no_strip_password, None).await?; + let meta = fungi::Writer::new(opts.meta, true).await?; + rfs::pack(meta, store, opts.target, !opts.no_strip_password).await?; Ok(()) }) @@ -240,3 +344,53 @@ async fn get_router(meta: &fungi::Reader) -> Result> { Ok(router) } + +fn clone(opts: CloneOptions) -> Result<()> { + let rt = tokio::runtime::Runtime::new()?; + + rt.block_on(async move { + let store = store::parse_router(opts.store.as_slice()).await?; + let meta = fungi::Reader::new(opts.meta) + .await + .context("failed to initialize metadata database")?; + + let router = get_router(&meta).await?; + + let cache = cache::Cache::new(opts.cache, router); + rfs::clone(meta, store, cache).await?; + + Ok(()) + }) +} +fn config(opts: ConfigOptions) -> Result<()> { + let rt = tokio::runtime::Runtime::new()?; + + rt.block_on(async move { + let writer = fungi::Writer::new(opts.meta.clone(), false) + .await + .context("failed to initialize metadata database")?; + + let reader = fungi::Reader::new(opts.meta) + .await + .context("failed to initialize metadata database")?; + + match opts.command { + ConfigCommands::Tag(opts) => match opts { + TagOperation::List => config::tag_list(reader).await?, + TagOperation::Add(opts) => config::tag_add(writer, opts.tag).await?, + TagOperation::Delete(opts) => { + config::tag_delete(writer, opts.key, opts.all).await? + } + }, + ConfigCommands::Store(opts) => match opts { + StoreOperation::List => config::store_list(reader).await?, + StoreOperation::Add(opts) => config::store_add(writer, opts.store).await?, + StoreOperation::Delete(opts) => { + config::store_delete(writer, opts.store, opts.all).await? + } + }, + } + + Ok(()) + }) +} diff --git a/rfs/src/store/dir.rs b/rfs/src/store/dir.rs index c99942e..c5e9b11 100644 --- a/rfs/src/store/dir.rs +++ b/rfs/src/store/dir.rs @@ -34,11 +34,24 @@ impl DirStore { #[async_trait::async_trait] impl Store for DirStore { async fn get(&self, key: &[u8]) -> Result> { - let path = self.root.join(hex::encode(key)); + let file_name = hex::encode(key); + let dir_path = self.root.join(&file_name[0..2]); + + let mut path = dir_path.join(&file_name); let data = match fs::read(&path).await { Ok(data) => data, Err(err) if err.kind() == ErrorKind::NotFound => { - return Err(Error::KeyNotFound); + path = self.root.join(file_name); + let data = match fs::read(&path).await { + Ok(data) => data, + Err(err) if err.kind() == ErrorKind::NotFound => { + return Err(Error::KeyNotFound); + } + Err(err) => { + return Err(Error::IO(err)); + } + }; + data } Err(err) => { return Err(Error::IO(err)); @@ -49,9 +62,13 @@ impl Store for DirStore { } async fn set(&self, key: &[u8], blob: &[u8]) -> Result<()> { - let path = self.root.join(hex::encode(key)); + let file_name = hex::encode(key); + let dir_path = self.root.join(&file_name[0..2]); + + fs::create_dir_all(&dir_path).await?; - fs::write(path, blob).await?; + let file_path = dir_path.join(file_name); + fs::write(file_path, blob).await?; Ok(()) } diff --git a/rfs/src/store/http.rs b/rfs/src/store/http.rs new file mode 100644 index 0000000..a3136fb --- /dev/null +++ b/rfs/src/store/http.rs @@ -0,0 +1,73 @@ +use super::{Error, Result, Route, Store}; +use reqwest::{self, StatusCode}; +use url::Url; + +#[derive(Clone)] +pub struct HTTPStore { + url: Url, +} + +impl HTTPStore { + pub async fn make>(url: &U) -> Result { + let u = Url::parse(url.as_ref())?; + if u.scheme() != "http" && u.scheme() != "https" { + return Err(Error::Other(anyhow::Error::msg("invalid scheme"))); + } + + Ok(HTTPStore::new(u).await?) + } + pub async fn new>(url: U) -> Result { + let url = url.into(); + Ok(Self { url }) + } +} + +#[async_trait::async_trait] +impl Store for HTTPStore { + async fn get(&self, key: &[u8]) -> Result> { + let file = hex::encode(key); + let mut file_path = self.url.clone(); + file_path + .path_segments_mut() + .map_err(|_| Error::Other(anyhow::Error::msg("cannot be base")))? + .push(&file[0..2]) + .push(&file); + let mut legacy_path = self.url.clone(); + + legacy_path + .path_segments_mut() + .map_err(|_| Error::Other(anyhow::Error::msg("cannot be base")))? + .push(&file); + + let data = match reqwest::get(file_path).await { + Ok(mut response) => { + if response.status() == StatusCode::NOT_FOUND { + response = reqwest::get(legacy_path) + .await + .map_err(|_| Error::KeyNotFound)?; + if response.status() != StatusCode::OK { + return Err(Error::KeyNotFound); + } + } + if response.status() != StatusCode::OK { + return Err(Error::Unavailable); + } + response.bytes().await.map_err(|e| Error::Other(e.into()))? + } + Err(err) => return Err(Error::Other(err.into())), + }; + Ok(data.into()) + } + + async fn set(&self, _key: &[u8], _blob: &[u8]) -> Result<()> { + Err(Error::Other(anyhow::Error::msg( + "http store doesn't support uploading", + ))) + } + + fn routes(&self) -> Vec { + let r = Route::url(self.url.clone()); + + vec![r] + } +} diff --git a/rfs/src/store/mod.rs b/rfs/src/store/mod.rs index 214faf1..d1e3c1e 100644 --- a/rfs/src/store/mod.rs +++ b/rfs/src/store/mod.rs @@ -1,5 +1,6 @@ mod bs; pub mod dir; +pub mod http; mod router; pub mod s3store; pub mod zdb; @@ -16,25 +17,10 @@ pub async fn make>(u: U) -> Result { let parsed = url::Url::parse(u.as_ref())?; match parsed.scheme() { - dir::SCHEME => { - return Ok(Stores::Dir( - dir::DirStore::make(&u) - .await - .expect("failed to make dir store"), - )) - } - "s3" | "s3s" | "s3s+tls" => { - return Ok(Stores::S3(s3store::S3Store::make(&u).await.expect( - format!("failed to make {} store", parsed.scheme()).as_str(), - ))) - } - "zdb" => { - return Ok(Stores::ZDB( - zdb::ZdbStore::make(&u) - .await - .expect("failed to make zdb store"), - )) - } + dir::SCHEME => return Ok(Stores::Dir(dir::DirStore::make(&u).await?)), + "s3" | "s3s" | "s3s+tls" => return Ok(Stores::S3(s3store::S3Store::make(&u).await?)), + "zdb" => return Ok(Stores::ZDB(zdb::ZdbStore::make(&u).await?)), + "http" | "https" => return Ok(Stores::HTTP(http::HTTPStore::make(&u).await?)), _ => return Err(Error::UnknownStore(parsed.scheme().into())), } } @@ -207,6 +193,7 @@ pub enum Stores { S3(s3store::S3Store), Dir(dir::DirStore), ZDB(zdb::ZdbStore), + HTTP(http::HTTPStore), } #[async_trait::async_trait] @@ -216,6 +203,7 @@ impl Store for Stores { self::Stores::S3(s3_store) => s3_store.get(key).await, self::Stores::Dir(dir_store) => dir_store.get(key).await, self::Stores::ZDB(zdb_store) => zdb_store.get(key).await, + self::Stores::HTTP(http_store) => http_store.get(key).await, } } async fn set(&self, key: &[u8], blob: &[u8]) -> Result<()> { @@ -223,6 +211,7 @@ impl Store for Stores { self::Stores::S3(s3_store) => s3_store.set(key, blob).await, self::Stores::Dir(dir_store) => dir_store.set(key, blob).await, self::Stores::ZDB(zdb_store) => zdb_store.set(key, blob).await, + self::Stores::HTTP(http_store) => http_store.set(key, blob).await, } } fn routes(&self) -> Vec { @@ -230,6 +219,7 @@ impl Store for Stores { self::Stores::S3(s3_store) => s3_store.routes(), self::Stores::Dir(dir_store) => dir_store.routes(), self::Stores::ZDB(zdb_store) => zdb_store.routes(), + self::Stores::HTTP(http_store) => http_store.routes(), } } }