init
This commit is contained in:
commit
783014b369
22 changed files with 5938 additions and 0 deletions
6
.env
Normal file
6
.env
Normal file
|
@ -0,0 +1,6 @@
|
|||
APPLICATION_BASE=https://oxidev.pfzetto.de/
|
||||
ISSUER=https://auth.zettoit.eu/realms/zettoit
|
||||
CLIENT_ID=oxicloud
|
||||
CLIENT_SECRET=IvBcDOfp9WBfGNmwIbiv67bxCwuQUGbl
|
||||
SCOPES=
|
||||
DATABASE_URL=mysql://root:start1234@127.0.0.1/oxicloud
|
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
|
@ -0,0 +1,3 @@
|
|||
/target
|
||||
/data
|
||||
.env
|
3308
Cargo.lock
generated
Normal file
3308
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
41
Cargo.toml
Normal file
41
Cargo.toml
Normal file
|
@ -0,0 +1,41 @@
|
|||
[package]
|
||||
name = "oxicloud"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
dotenvy = "0.15.7"
|
||||
log = "0.4.20"
|
||||
env_logger = "0.10.1"
|
||||
thiserror = "1.0.51"
|
||||
|
||||
tokio = { version = "1.35.1", features = ["full"] }
|
||||
axum = { version = "0.7.2", features = [ "macros" ] }
|
||||
axum-oidc = "0.2.1"
|
||||
|
||||
serde = { version = "1.0.193", features = [ "derive" ] }
|
||||
serde-xml-rs = "0.6.0"
|
||||
serde_json = "1.0.108"
|
||||
|
||||
webdav-handler = { git="https://github.com/pfzetto/webdav-handler-rs" }
|
||||
|
||||
tower = "0.4.13"
|
||||
tower-http = { version = "0.5.0", features = [ "trace" ] }
|
||||
tower-sessions = "0.7.0"
|
||||
|
||||
sqlx = { version="0.7.3", features=["runtime-tokio", "mysql", "time"] }
|
||||
|
||||
sha3 = "0.10.8"
|
||||
md5 = "0.7.0"
|
||||
rand = "0.8.5"
|
||||
base64 = "0.21.5"
|
||||
|
||||
bytes = "1.5.0"
|
||||
futures = "0.3.29"
|
||||
|
||||
xmltree = "0.10.3"
|
||||
xml = "0.8.10"
|
||||
|
||||
time = "0.3.31"
|
21
README.md
Normal file
21
README.md
Normal file
|
@ -0,0 +1,21 @@
|
|||
Oxicloud is a file server that aims to be a minimal implementation of a Nextcloud combatible client API.
|
||||
|
||||
# Disclaimer
|
||||
Please report any bugs you find using Oxicloud to this project.
|
||||
Do not report issues at the Nexclound clients.
|
||||
|
||||
# Features
|
||||
## General
|
||||
- [x] File browsing
|
||||
- [x] Folder creation
|
||||
- [x] Small file upload
|
||||
- [ ] Large file upload
|
||||
- [x] File deletion
|
||||
|
||||
## Sharing
|
||||
- [ ] User shares
|
||||
- [ ] Link sharing
|
||||
|
||||
## Preview
|
||||
- [ ] Images
|
||||
- [ ] Documents
|
12
TODO.md
Normal file
12
TODO.md
Normal file
|
@ -0,0 +1,12 @@
|
|||
- XML permissions field dynamic
|
||||
|
||||
|
||||
# XML `oc:permissions`
|
||||
|Permission|Meaning|
|
||||
|---|---|
|
||||
|`CK`|Can Write|
|
||||
|`S`| Shared with me|
|
||||
|`R`|Can Reshare|
|
||||
|`M`|Groupfolder|
|
||||
|
||||
|
106
flake.lock
Normal file
106
flake.lock
Normal file
|
@ -0,0 +1,106 @@
|
|||
{
|
||||
"nodes": {
|
||||
"crane": {
|
||||
"inputs": {
|
||||
"nixpkgs": [
|
||||
"nixpkgs"
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1703089493,
|
||||
"narHash": "sha256-WUjYqUP/Lhhop9+aiHVFREgElunx1AHEWxqMT8ePfzo=",
|
||||
"owner": "ipetkov",
|
||||
"repo": "crane",
|
||||
"rev": "2a5136f14a9ac93d9d370d64a36026c5de3ae8a4",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "ipetkov",
|
||||
"repo": "crane",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"flake-utils": {
|
||||
"inputs": {
|
||||
"systems": "systems"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1701680307,
|
||||
"narHash": "sha256-kAuep2h5ajznlPMD9rnQyffWG8EM/C73lejGofXvdM8=",
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"rev": "4022d587cbbfd70fe950c1e2083a02621806a725",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1703013332,
|
||||
"narHash": "sha256-+tFNwMvlXLbJZXiMHqYq77z/RfmpfpiI3yjL6o/Zo9M=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "54aac082a4d9bb5bbc5c4e899603abfb76a3f6d6",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "NixOS",
|
||||
"ref": "nixos-unstable",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"root": {
|
||||
"inputs": {
|
||||
"crane": "crane",
|
||||
"flake-utils": "flake-utils",
|
||||
"nixpkgs": "nixpkgs",
|
||||
"rust-overlay": "rust-overlay"
|
||||
}
|
||||
},
|
||||
"rust-overlay": {
|
||||
"inputs": {
|
||||
"flake-utils": [
|
||||
"flake-utils"
|
||||
],
|
||||
"nixpkgs": [
|
||||
"nixpkgs"
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1703124916,
|
||||
"narHash": "sha256-LNAqNYcJf0iCm6jbzhzsQOC4F8SLyma5sckySn2Iffg=",
|
||||
"owner": "oxalica",
|
||||
"repo": "rust-overlay",
|
||||
"rev": "81cb529bd066cd3668f9aa88d2afa8fbbbcd1208",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "oxalica",
|
||||
"repo": "rust-overlay",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"systems": {
|
||||
"locked": {
|
||||
"lastModified": 1681028828,
|
||||
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"type": "github"
|
||||
}
|
||||
}
|
||||
},
|
||||
"root": "root",
|
||||
"version": 7
|
||||
}
|
75
flake.nix
Normal file
75
flake.nix
Normal file
|
@ -0,0 +1,75 @@
|
|||
{
|
||||
description = "oxicloud service";
|
||||
|
||||
inputs = {
|
||||
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
|
||||
flake-utils.url = "github:numtide/flake-utils";
|
||||
rust-overlay = {
|
||||
url = "github:oxalica/rust-overlay";
|
||||
inputs = {
|
||||
nixpkgs.follows = "nixpkgs";
|
||||
flake-utils.follows = "flake-utils";
|
||||
};
|
||||
};
|
||||
crane = {
|
||||
url = "github:ipetkov/crane";
|
||||
inputs = {
|
||||
nixpkgs.follows = "nixpkgs";
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
outputs = { self, nixpkgs, flake-utils, rust-overlay, crane}: let
|
||||
forAllSystems = function:
|
||||
nixpkgs.lib.genAttrs [
|
||||
"x86_64-linux"
|
||||
"aarch64-linux"
|
||||
] (system: function system nixpkgs.legacyPackages.${system});
|
||||
in rec {
|
||||
packages = forAllSystems(system: syspkgs: let
|
||||
pkgs = import nixpkgs {
|
||||
inherit system;
|
||||
overlays = [ (import rust-overlay) ];
|
||||
};
|
||||
rustToolchain = pkgs.rust-bin.nightly.latest.default;
|
||||
|
||||
craneLib = (crane.mkLib pkgs).overrideToolchain rustToolchain;
|
||||
src = pkgs.lib.cleanSourceWith {
|
||||
src = craneLib.path ./.;
|
||||
filter = path: type:
|
||||
(pkgs.lib.hasSuffix "\.stpl" path) ||
|
||||
(pkgs.lib.hasInfix "static" path) ||
|
||||
(craneLib.filterCargoSources path type)
|
||||
;
|
||||
};
|
||||
|
||||
nativeBuildInputs = with pkgs; [ rustToolchain pkg-config ];
|
||||
buildInputs = with pkgs; [ ];
|
||||
|
||||
commonArgs = {
|
||||
inherit src buildInputs nativeBuildInputs;
|
||||
};
|
||||
cargoArtifacts = craneLib.buildDepsOnly commonArgs;
|
||||
|
||||
bin = craneLib.buildPackage (commonArgs // {
|
||||
inherit cargoArtifacts;
|
||||
pname = "oxicloud";
|
||||
installPhaseCommand = ''
|
||||
mkdir -p $out/bin
|
||||
cp target/release/oxicloud $out/bin/oxicloud
|
||||
cp -r static $out/static
|
||||
'';
|
||||
});
|
||||
in {
|
||||
inherit bin;
|
||||
default = bin;
|
||||
});
|
||||
devShells = forAllSystems(system: pkgs: {
|
||||
default = pkgs.mkShell {
|
||||
packages = with pkgs; [ sqlx-cli cargo-watch mysql-client ];
|
||||
inputsFrom = [ packages.${system}.bin ];
|
||||
};
|
||||
});
|
||||
hydraJobs."bin" = forAllSystems(system: pkgs: packages.${system}.bin);
|
||||
};
|
||||
}
|
20
migrations/20231220121329_users.sql
Normal file
20
migrations/20231220121329_users.sql
Normal file
|
@ -0,0 +1,20 @@
|
|||
-- Add migration script here
|
||||
|
||||
CREATE TABLE users (
|
||||
id INT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
oidc_id VARCHAR(48) NOT NULL,
|
||||
name VARCHAR(128) NOT NULL,
|
||||
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE (oidc_id),
|
||||
FULLTEXT (name)
|
||||
);
|
||||
|
||||
CREATE TABLE user_tokens (
|
||||
id int UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
user_id INT UNSIGNED NOT NULL,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
hash BINARY(32) NOT NULL,
|
||||
PRIMARY KEY (id),
|
||||
CONSTRAINT fk_users_app_passords FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
||||
);
|
19
migrations/20231220211536_shares.sql
Normal file
19
migrations/20231220211536_shares.sql
Normal file
|
@ -0,0 +1,19 @@
|
|||
-- Add migration script here
|
||||
|
||||
CREATE TABLE user_shares (
|
||||
id INT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
created_by INT UNSIGNED NOT NULL,
|
||||
src_user INT UNSIGNED NOT NULL,
|
||||
src_path TEXT NOT NULL,
|
||||
dst_user INT UNSIGNED NOT NULL,
|
||||
dst_path TEXT NOT NULL,
|
||||
expires_at DATETIME NULL,
|
||||
note TEXT NOT NULL,
|
||||
permissions TINYINT UNSIGNED NOT NULL,
|
||||
|
||||
PRIMARY KEY (id),
|
||||
CONSTRAINT fk_user_shares_src FOREIGN KEY (src_user) REFERENCES users(id) ON DELETE CASCADE,
|
||||
CONSTRAINT fk_user_shares_dst FOREIGN KEY (dst_user) REFERENCES users(id) ON DELETE CASCADE,
|
||||
CONSTRAINT fk_user_shares_created_by FOREIGN KEY (created_by) REFERENCES users(id) ON DELETE CASCADE,
|
||||
FULLTEXT (src_path, dst_path)
|
||||
);
|
751
src/dav_fs.rs
Normal file
751
src/dav_fs.rs
Normal file
|
@ -0,0 +1,751 @@
|
|||
use std::{
|
||||
collections::{BTreeMap, HashMap},
|
||||
fs::Metadata,
|
||||
future::ready,
|
||||
io::SeekFrom,
|
||||
path::{Path, PathBuf},
|
||||
pin::Pin,
|
||||
sync::Arc,
|
||||
task::{Context, Poll},
|
||||
time::{SystemTime, UNIX_EPOCH},
|
||||
};
|
||||
|
||||
use axum::http::StatusCode;
|
||||
use base64::{engine::general_purpose::STANDARD as base64std, Engine};
|
||||
use bytes::{Buf, Bytes, BytesMut};
|
||||
use futures::{future::BoxFuture, stream, Future, FutureExt, Stream, StreamExt, TryFutureExt};
|
||||
use log::{error, info};
|
||||
use sqlx::{query, MySqlPool};
|
||||
use tokio::{
|
||||
fs::{metadata, read_dir, File, ReadDir},
|
||||
io::{AsyncReadExt, AsyncSeekExt, AsyncWriteExt},
|
||||
sync::RwLock,
|
||||
};
|
||||
use webdav_handler::{
|
||||
davpath::DavPath,
|
||||
fs::{
|
||||
DavDirEntry, DavFile, DavFileSystem, DavMetaData, DavProp, FsError, FsFuture, FsResult,
|
||||
FsStream, OpenOptions, ReadDirMeta,
|
||||
},
|
||||
};
|
||||
use xmltree::{Element, XMLNode};
|
||||
|
||||
use crate::{error::Error, fs::FsUser};
|
||||
|
||||
const DAV_OC_PROP: &str = "http://owncloud.org/ns";
|
||||
const DAV_NC_PROP: &str = "http://nextcloud.org/ns";
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct FilesFs {
|
||||
pub user: FsUser,
|
||||
pub db: MySqlPool,
|
||||
pub incoming_share_cache: Arc<RwLock<BTreeMap<PathBuf, (FsUser, PathBuf)>>>,
|
||||
}
|
||||
|
||||
impl DavFileSystem for FilesFs {
|
||||
fn open<'a>(&'a self, path: &'a DavPath, options: OpenOptions) -> FsFuture<Box<dyn DavFile>> {
|
||||
info!("test: {}", path.to_string());
|
||||
Box::pin(async move {
|
||||
let (meta, path) = self
|
||||
.resolve_path(path)
|
||||
.map_err(|_| FsError::GeneralFailure)
|
||||
.await?;
|
||||
|
||||
let mut tokio_options = tokio::fs::OpenOptions::new();
|
||||
tokio_options
|
||||
.read(options.read)
|
||||
.write(options.write)
|
||||
.append(options.append)
|
||||
.truncate(options.truncate)
|
||||
.create(options.create)
|
||||
.create_new(options.create_new);
|
||||
|
||||
let file = OxiFile::new(path.clone(), &tokio_options).await?;
|
||||
let file: Box<dyn DavFile> = Box::new(file);
|
||||
|
||||
Ok(file)
|
||||
})
|
||||
}
|
||||
|
||||
fn read_dir<'a>(
|
||||
&'a self,
|
||||
ppath: &'a DavPath,
|
||||
meta: ReadDirMeta,
|
||||
) -> FsFuture<FsStream<Box<dyn DavDirEntry>>> {
|
||||
info!("dir: {:?}", ppath.as_pathbuf());
|
||||
Box::pin(async move {
|
||||
//let (is_share, path) = self
|
||||
// .resolve_path(ppath)
|
||||
// .map_err(|_| FsError::GeneralFailure)
|
||||
// .await?;
|
||||
//warn!("dirpath: {:?}", path);
|
||||
//if !self.can_access(&path) {
|
||||
// return Err(FsError::Forbidden);
|
||||
//}
|
||||
|
||||
let (meta, path) = self
|
||||
.resolve_path(ppath)
|
||||
.map_err(|_| FsError::GeneralFailure)
|
||||
.await?;
|
||||
|
||||
let entry_stream = DirEntryStream::read_dir(&path).await.map(|x| {
|
||||
let y: Box<dyn DavDirEntry> = Box::new(x);
|
||||
y
|
||||
});
|
||||
|
||||
let entry_stream = entry_stream.chain(stream::iter(self.shares(&ppath).await.unwrap()));
|
||||
let entry_stream: FsStream<Box<dyn DavDirEntry>> = Box::pin(entry_stream);
|
||||
|
||||
Ok(entry_stream)
|
||||
})
|
||||
}
|
||||
|
||||
fn metadata<'a>(&'a self, path: &'a DavPath) -> FsFuture<Box<dyn DavMetaData>> {
|
||||
Box::pin(async move {
|
||||
info!("metadata {:?}", path);
|
||||
let (meta, path) = self
|
||||
.resolve_path(path)
|
||||
.map_err(|_| FsError::GeneralFailure)
|
||||
.await?;
|
||||
|
||||
if meta.exists {
|
||||
let file = OxiFile::new(path.clone(), tokio::fs::OpenOptions::new().read(true))
|
||||
.await?
|
||||
.metadata()
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
Ok(file)
|
||||
} else {
|
||||
Err(FsError::NotFound)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn symlink_metadata<'a>(&'a self, path: &'a DavPath) -> FsFuture<Box<dyn DavMetaData>> {
|
||||
// symlinks are currently not supported, so no difference to normal metadata
|
||||
self.metadata(path)
|
||||
}
|
||||
|
||||
fn create_dir<'a>(&'a self, path: &'a DavPath) -> FsFuture<()> {
|
||||
Box::pin(async move {
|
||||
info!("create dir");
|
||||
let (path_meta, path) = self
|
||||
.resolve_path(path)
|
||||
.map_err(|_| FsError::GeneralFailure)
|
||||
.await?;
|
||||
|
||||
match path_meta.exists {
|
||||
false => {
|
||||
//TODO remove shares in db with src_path=path
|
||||
tokio::fs::create_dir_all(path)
|
||||
.await
|
||||
.map_err(|_| FsError::GeneralFailure)?;
|
||||
Ok(())
|
||||
}
|
||||
true => Err(FsError::Exists),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn remove_dir<'a>(&'a self, path: &'a DavPath) -> FsFuture<()> {
|
||||
Box::pin(async move {
|
||||
info!("remove dir");
|
||||
let (path_meta, path) = self
|
||||
.resolve_path(path)
|
||||
.map_err(|_| FsError::GeneralFailure)
|
||||
.await?;
|
||||
|
||||
match (path_meta.exists, path_meta.is_mount_point) {
|
||||
(true, false) => {
|
||||
//TODO remove shares in db with src_path.starts_with(path)
|
||||
tokio::fs::remove_dir(path)
|
||||
.await
|
||||
.map_err(|_| FsError::GeneralFailure)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
(true, true) => Err(FsError::NotImplemented), //TODO remove share in db with dst_path =path
|
||||
_ => Err(FsError::NotFound),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn remove_file<'a>(&'a self, path: &'a DavPath) -> FsFuture<()> {
|
||||
Box::pin(async move {
|
||||
info!("remove file");
|
||||
|
||||
let (path_meta, path) = self
|
||||
.resolve_path(path)
|
||||
.map_err(|_| FsError::GeneralFailure)
|
||||
.await?;
|
||||
|
||||
match (path_meta.exists, path_meta.is_mount_point) {
|
||||
(true, false) => {
|
||||
//TODO remove shares in db with src_path=path
|
||||
tokio::fs::remove_file(path)
|
||||
.await
|
||||
.map_err(|_| FsError::GeneralFailure)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
(true, true) => Err(FsError::NotImplemented), //TODO remove share in db with dst_path =path
|
||||
_ => Err(FsError::NotFound),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn rename<'a>(&'a self, from: &'a DavPath, to: &'a DavPath) -> FsFuture<()> {
|
||||
Box::pin(async move {
|
||||
info!("rename");
|
||||
let (from_meta, from) = self
|
||||
.resolve_path(from)
|
||||
.map_err(|_| FsError::GeneralFailure)
|
||||
.await?;
|
||||
let (to_meta, to) = self
|
||||
.resolve_path(to)
|
||||
.map_err(|_| FsError::GeneralFailure)
|
||||
.await?;
|
||||
|
||||
match (from_meta.exists, to_meta.exists) {
|
||||
(true, false) if !from_meta.is_mount_point => {
|
||||
tokio::fs::rename(from, to)
|
||||
.await
|
||||
.map_err(|_| FsError::GeneralFailure)?;
|
||||
Ok(())
|
||||
}
|
||||
(true, false) => Err(FsError::NotImplemented), //TODO rename dst in db
|
||||
(false, _) => Err(FsError::NotFound),
|
||||
(true, true) => Err(FsError::Exists),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn copy<'a>(&'a self, from: &'a DavPath, to: &'a DavPath) -> FsFuture<()> {
|
||||
Box::pin(async move {
|
||||
info!("copy");
|
||||
let (from_meta, from) = self
|
||||
.resolve_path(from)
|
||||
.map_err(|_| FsError::GeneralFailure)
|
||||
.await?;
|
||||
let (to_meta, to) = self
|
||||
.resolve_path(to)
|
||||
.map_err(|_| FsError::GeneralFailure)
|
||||
.await?;
|
||||
match (from_meta.exists, to_meta.exists) {
|
||||
(true, false) => {
|
||||
tokio::fs::copy(from, to)
|
||||
.await
|
||||
.map_err(|_| FsError::GeneralFailure)?;
|
||||
}
|
||||
(false, _) => return Err(FsError::NotFound),
|
||||
(true, true) => return Err(FsError::Exists),
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
fn have_props<'a>(
|
||||
&'a self,
|
||||
path: &'a DavPath,
|
||||
) -> std::pin::Pin<Box<dyn Future<Output = bool> + Send + 'a>> {
|
||||
Box::pin(ready(true))
|
||||
}
|
||||
|
||||
fn patch_props<'a>(
|
||||
&'a self,
|
||||
path: &'a DavPath,
|
||||
patch: Vec<(bool, DavProp)>,
|
||||
) -> FsFuture<Vec<(StatusCode, DavProp)>> {
|
||||
Box::pin(async move {
|
||||
error!("NOT IMPLEMENTED patch_props");
|
||||
Err(FsError::NotImplemented)
|
||||
})
|
||||
}
|
||||
|
||||
fn get_props<'a>(&'a self, path: &'a DavPath, do_content: bool) -> FsFuture<Vec<DavProp>> {
|
||||
Box::pin(async move {
|
||||
error!("NOT IMPLEMENTED get_props");
|
||||
Err(FsError::NotImplemented)
|
||||
})
|
||||
}
|
||||
|
||||
fn get_prop<'a>(&'a self, ppath: &'a DavPath, prop: DavProp) -> FsFuture<Element> {
|
||||
fn element(prefix: &str, name: &str, children: Vec<XMLNode>) -> Element {
|
||||
Element {
|
||||
prefix: Some(prefix.to_string()),
|
||||
namespace: None,
|
||||
namespaces: None,
|
||||
name: name.to_string(),
|
||||
attributes: HashMap::default(),
|
||||
children,
|
||||
}
|
||||
}
|
||||
|
||||
Box::pin(async move {
|
||||
let (meta, path) = self
|
||||
.resolve_path(ppath)
|
||||
.map_err(|_| FsError::GeneralFailure)
|
||||
.await?;
|
||||
//if !self.can_access(&path) {
|
||||
// return Err(FsError::Forbidden);
|
||||
//}
|
||||
//
|
||||
const NC_PREFIX: &str = "nc";
|
||||
const OC_PREFIX: &str = "oc";
|
||||
|
||||
match (
|
||||
prop.name.as_str(),
|
||||
prop.namespace.as_deref().unwrap_or_default(),
|
||||
) {
|
||||
("permissions", DAV_OC_PROP) => {
|
||||
let val = match meta.is_share {
|
||||
true => "SRGDNVCK",
|
||||
false => "RGDNVCK",
|
||||
};
|
||||
return Ok(element(
|
||||
OC_PREFIX,
|
||||
"permissions",
|
||||
vec![XMLNode::Text(val.to_string())],
|
||||
));
|
||||
}
|
||||
("rich-workspace", DAV_NC_PROP) if false => {
|
||||
return Ok(element(
|
||||
NC_PREFIX,
|
||||
"rich-workspace",
|
||||
vec![XMLNode::Text(
|
||||
"# Hello World\nLorem Ipsum, si dolor amet.".to_string(),
|
||||
)],
|
||||
))
|
||||
}
|
||||
("has-preview", DAV_NC_PROP) => {
|
||||
return Ok(element(
|
||||
NC_PREFIX,
|
||||
"has-preview",
|
||||
vec![XMLNode::Text("false".to_string())],
|
||||
))
|
||||
}
|
||||
("mount-type", DAV_NC_PROP) => {
|
||||
let val = match meta.is_share {
|
||||
true => "shared",
|
||||
false => "",
|
||||
};
|
||||
return Ok(element(
|
||||
NC_PREFIX,
|
||||
"mount-type",
|
||||
vec![XMLNode::Text(val.to_string())],
|
||||
));
|
||||
}
|
||||
("sharees", DAV_NC_PROP) if false => {
|
||||
return Ok(element(
|
||||
NC_PREFIX,
|
||||
"sharees",
|
||||
vec![XMLNode::Element(element(
|
||||
"sharee",
|
||||
"nc",
|
||||
vec![
|
||||
XMLNode::Element(element(
|
||||
"id",
|
||||
"nc",
|
||||
vec![XMLNode::Text("1".to_string())],
|
||||
)),
|
||||
XMLNode::Element(element(
|
||||
"display-name",
|
||||
"nc",
|
||||
vec![XMLNode::Text("testuser".to_string())],
|
||||
)),
|
||||
XMLNode::Element(element(
|
||||
"type",
|
||||
"nc",
|
||||
vec![XMLNode::Text("0".to_string())],
|
||||
)),
|
||||
],
|
||||
))],
|
||||
));
|
||||
}
|
||||
("is-encrypted", DAV_NC_PROP) => {
|
||||
return Ok(element(
|
||||
NC_PREFIX,
|
||||
"is-encrypted",
|
||||
vec![XMLNode::Text("false".to_string())],
|
||||
))
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
Err(FsError::NotFound)
|
||||
})
|
||||
}
|
||||
|
||||
fn get_quota(&self) -> FsFuture<(u64, Option<u64>)> {
|
||||
info!("quota");
|
||||
Box::pin(async move {
|
||||
error!("NOT IMPLEMENTED get_quota");
|
||||
Err(FsError::NotImplemented)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl FilesFs {
|
||||
async fn fill_cache(&self) -> Result<(), Error> {
|
||||
if self.incoming_share_cache.read().await.is_empty() {
|
||||
let res = query!(
|
||||
"SELECT src_user, src_path, dst_path FROM user_shares WHERE dst_user = ?",
|
||||
self.user.0
|
||||
)
|
||||
.fetch_all(&self.db)
|
||||
.await?;
|
||||
|
||||
let mut cache = self.incoming_share_cache.write().await;
|
||||
res.into_iter().for_each(|x| {
|
||||
cache.insert(
|
||||
PathBuf::from(x.dst_path),
|
||||
(FsUser(x.src_user), PathBuf::from(x.src_path)),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn shares(&self, rel_root: &DavPath) -> Result<Vec<Box<dyn DavDirEntry>>, Error> {
|
||||
let rel_dst_home = rel_root.as_pathbuf();
|
||||
let rel_dst_home = rel_dst_home
|
||||
.strip_prefix("/")
|
||||
.map(|x| x.to_owned())
|
||||
.unwrap_or_else(|_| rel_dst_home);
|
||||
|
||||
self.fill_cache().await?;
|
||||
|
||||
let cache = self.incoming_share_cache.read().await;
|
||||
|
||||
let mut cursor = cache.lower_bound(std::ops::Bound::Included(&rel_dst_home));
|
||||
|
||||
let mut share_dir_entries: Vec<Box<dyn DavDirEntry>> = vec![];
|
||||
|
||||
while let Some((dst_path, (src_user, src_path))) =
|
||||
cursor
|
||||
.key_value()
|
||||
.and_then(|x| match x.0.parent() == Some(&rel_dst_home) {
|
||||
true => Some(x),
|
||||
false => None,
|
||||
})
|
||||
{
|
||||
let name = dst_path.strip_prefix(&rel_dst_home).unwrap();
|
||||
|
||||
let mut p = PathBuf::new();
|
||||
p.push(src_user.home_dir());
|
||||
p.push(src_path);
|
||||
|
||||
let meta = metadata(&p).await.unwrap();
|
||||
|
||||
share_dir_entries.push(Box::new(OxiDirEntry {
|
||||
name: name.to_str().unwrap_or_default().into(),
|
||||
len: meta.len(),
|
||||
dir: p.is_dir(),
|
||||
etag: etag(&p, &meta),
|
||||
modified: meta.modified().ok(),
|
||||
created: meta.created().ok(),
|
||||
}));
|
||||
|
||||
cursor.move_next();
|
||||
}
|
||||
|
||||
Ok(share_dir_entries)
|
||||
}
|
||||
|
||||
async fn resolve_path(&self, rel_root: &DavPath) -> Result<(PathMeta, PathBuf), Error> {
|
||||
let rel_dst_home = rel_root.as_pathbuf();
|
||||
let rel_dst_home = rel_dst_home
|
||||
.strip_prefix("/")
|
||||
.map(|x| x.to_owned())
|
||||
.unwrap_or_else(|_| rel_dst_home);
|
||||
|
||||
self.fill_cache().await?;
|
||||
|
||||
let cache = self.incoming_share_cache.read().await;
|
||||
|
||||
let cursor = cache.lower_bound(std::ops::Bound::Included(&rel_dst_home));
|
||||
let share = cursor
|
||||
.key_value()
|
||||
.and_then(|x| rel_dst_home.strip_prefix(x.0).map(|y| (y, x.1)).ok());
|
||||
|
||||
if let Some((rel_mnt, (src_user, src_path))) = share {
|
||||
let mut p = PathBuf::new();
|
||||
p.push(src_user.home_dir());
|
||||
p.push(src_path);
|
||||
p.push(rel_mnt);
|
||||
|
||||
Ok((
|
||||
PathMeta {
|
||||
is_share: true,
|
||||
is_mount_point: rel_mnt == rel_dst_home,
|
||||
owner: *src_user,
|
||||
exists: p.exists(),
|
||||
},
|
||||
p,
|
||||
))
|
||||
} else {
|
||||
let mut p = PathBuf::new();
|
||||
p.push(&self.user.home_dir());
|
||||
p.push(rel_dst_home.clone());
|
||||
|
||||
Ok((
|
||||
PathMeta {
|
||||
is_share: false,
|
||||
is_mount_point: false,
|
||||
owner: self.user,
|
||||
exists: p.exists(),
|
||||
},
|
||||
p,
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct PathMeta {
|
||||
is_share: bool,
|
||||
is_mount_point: bool,
|
||||
owner: FsUser,
|
||||
exists: bool,
|
||||
}
|
||||
|
||||
pub struct DirEntryStream<'a> {
|
||||
entry_stream: ReadDir,
|
||||
entry_stream_completed: bool,
|
||||
intermediate: Vec<BoxFuture<'a, OxiDirEntry>>,
|
||||
}
|
||||
|
||||
impl<'a> DirEntryStream<'a> {
|
||||
pub async fn read_dir(path: &Path) -> DirEntryStream<'a> {
|
||||
Self {
|
||||
entry_stream: read_dir(path).await.unwrap(),
|
||||
entry_stream_completed: false,
|
||||
intermediate: vec![],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Stream for DirEntryStream<'a> {
|
||||
type Item = OxiDirEntry;
|
||||
|
||||
fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
|
||||
let entry_stream_item = match self.entry_stream_completed {
|
||||
false => self.entry_stream.poll_next_entry(cx),
|
||||
true => Poll::Ready(Ok(None)),
|
||||
};
|
||||
match entry_stream_item {
|
||||
Poll::Ready(Ok(Some(entry))) => self.intermediate.push(Box::pin(async move {
|
||||
let meta = entry.metadata().await.unwrap();
|
||||
OxiDirEntry {
|
||||
name: entry.file_name().to_str().unwrap_or_default().into(),
|
||||
len: meta.len(),
|
||||
dir: entry.file_type().await.unwrap().is_dir(),
|
||||
etag: etag(&entry.path(), &meta),
|
||||
modified: meta.modified().ok(),
|
||||
created: meta.created().ok(),
|
||||
}
|
||||
})),
|
||||
Poll::Ready(Ok(None)) => {
|
||||
self.entry_stream_completed = true;
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
for (i, intermediate) in self.intermediate.iter_mut().enumerate() {
|
||||
if let Poll::Ready(x) = intermediate.poll_unpin(cx) {
|
||||
let intermediate_fut = self.intermediate.remove(i);
|
||||
drop(intermediate_fut);
|
||||
return Poll::Ready(Some(x));
|
||||
}
|
||||
}
|
||||
match self.intermediate.is_empty() {
|
||||
true => Poll::Ready(None),
|
||||
false => Poll::Pending,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct OxiDirEntry {
|
||||
name: Box<str>,
|
||||
len: u64,
|
||||
dir: bool,
|
||||
etag: String,
|
||||
modified: Option<SystemTime>,
|
||||
created: Option<SystemTime>,
|
||||
}
|
||||
|
||||
impl DavDirEntry for OxiDirEntry {
|
||||
fn name(&self) -> Vec<u8> {
|
||||
self.name.as_bytes().to_vec()
|
||||
}
|
||||
|
||||
fn metadata(&self) -> FsFuture<Box<dyn DavMetaData>> {
|
||||
Box::pin(async move {
|
||||
let meta: Box<dyn DavMetaData> = Box::new(self.clone());
|
||||
Ok(meta)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl DavMetaData for OxiDirEntry {
|
||||
fn len(&self) -> u64 {
|
||||
self.len
|
||||
}
|
||||
|
||||
fn modified(&self) -> FsResult<std::time::SystemTime> {
|
||||
self.modified.ok_or(FsError::GeneralFailure)
|
||||
}
|
||||
|
||||
fn created(&self) -> FsResult<std::time::SystemTime> {
|
||||
self.created.ok_or(FsError::GeneralFailure)
|
||||
}
|
||||
|
||||
fn is_dir(&self) -> bool {
|
||||
self.dir
|
||||
}
|
||||
|
||||
fn etag(&self) -> Option<String> {
|
||||
Some(self.etag.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct OxiFile {
|
||||
path: PathBuf,
|
||||
file: Option<File>,
|
||||
}
|
||||
|
||||
impl OxiFile {
|
||||
pub async fn new(path: PathBuf, options: &tokio::fs::OpenOptions) -> FsResult<Self> {
|
||||
let file = options
|
||||
.open(&path)
|
||||
.await
|
||||
.map_err(|_| FsError::GeneralFailure)?;
|
||||
Ok(Self {
|
||||
path,
|
||||
file: Some(file),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl DavFile for OxiFile {
|
||||
fn metadata(&mut self) -> FsFuture<Box<dyn DavMetaData>> {
|
||||
Box::pin(async move {
|
||||
let meta = metadata(&self.path).await.unwrap();
|
||||
|
||||
let etag = etag(&self.path, &meta);
|
||||
|
||||
let oxi_meta = OxiFileMeta {
|
||||
len: meta.len(),
|
||||
is_dir: meta.is_dir(),
|
||||
etag,
|
||||
};
|
||||
let oxi_meta: Box<dyn DavMetaData> = Box::new(oxi_meta);
|
||||
Ok(oxi_meta)
|
||||
})
|
||||
}
|
||||
|
||||
fn write_bytes(&mut self, buf: Bytes) -> FsFuture<()> {
|
||||
async move {
|
||||
let mut file = self.file.take().unwrap();
|
||||
let res = file.write_all(&buf).await;
|
||||
self.file = Some(file);
|
||||
res.map_err(|e| e.into())
|
||||
}
|
||||
.boxed()
|
||||
}
|
||||
|
||||
fn write_buf(&mut self, mut buf: Box<dyn Buf + Send>) -> FsFuture<()> {
|
||||
async move {
|
||||
let mut file = self.file.take().unwrap();
|
||||
while buf.remaining() > 0 {
|
||||
match file.write(buf.chunk()).await {
|
||||
Ok(n) => buf.advance(n),
|
||||
Err(e) => {
|
||||
self.file = Some(file);
|
||||
return Err(e.into());
|
||||
}
|
||||
}
|
||||
}
|
||||
self.file = Some(file);
|
||||
Ok(())
|
||||
}
|
||||
.boxed()
|
||||
}
|
||||
|
||||
fn read_bytes(&mut self, count: usize) -> FsFuture<Bytes> {
|
||||
async move {
|
||||
let mut file = self.file.take().unwrap();
|
||||
let mut buf = BytesMut::with_capacity(count);
|
||||
let res = unsafe {
|
||||
buf.set_len(count);
|
||||
file.read(&mut buf).await.map(|n| {
|
||||
buf.set_len(n);
|
||||
buf.freeze()
|
||||
})
|
||||
};
|
||||
self.file = Some(file);
|
||||
res.map_err(|e| e.into())
|
||||
}
|
||||
.boxed()
|
||||
}
|
||||
|
||||
fn seek(&mut self, pos: SeekFrom) -> FsFuture<u64> {
|
||||
async move {
|
||||
let mut file = self.file.take().unwrap();
|
||||
let res = file.seek(pos).await;
|
||||
self.file = Some(file);
|
||||
res.map_err(|e| e.into())
|
||||
}
|
||||
.boxed()
|
||||
}
|
||||
|
||||
fn flush(&mut self) -> FsFuture<()> {
|
||||
async move {
|
||||
let mut file = self.file.take().unwrap();
|
||||
let res = file.flush().await;
|
||||
self.file = Some(file);
|
||||
res.map_err(|e| e.into())
|
||||
}
|
||||
.boxed()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct OxiFileMeta {
|
||||
len: u64,
|
||||
is_dir: bool,
|
||||
etag: String,
|
||||
}
|
||||
|
||||
impl DavMetaData for OxiFileMeta {
|
||||
fn len(&self) -> u64 {
|
||||
self.len
|
||||
}
|
||||
|
||||
fn modified(&self) -> FsResult<SystemTime> {
|
||||
Err(FsError::NotImplemented)
|
||||
}
|
||||
|
||||
fn is_dir(&self) -> bool {
|
||||
self.is_dir
|
||||
}
|
||||
|
||||
fn etag(&self) -> Option<String> {
|
||||
Some(self.etag.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
fn etag(path: &Path, meta: &Metadata) -> String {
|
||||
let modified = meta
|
||||
.modified()
|
||||
.ok()
|
||||
.and_then(|x| x.duration_since(UNIX_EPOCH).ok())
|
||||
.map(|x| x.as_secs().to_ne_bytes())
|
||||
.unwrap_or_default()
|
||||
.to_vec();
|
||||
let path = path.to_str().unwrap_or_default().as_bytes();
|
||||
let hash: [u8; 16] = md5::compute([path, &modified].concat()).into();
|
||||
base64std.encode(hash)
|
||||
}
|
46
src/error.rs
Normal file
46
src/error.rs
Normal file
|
@ -0,0 +1,46 @@
|
|||
use axum::{http::StatusCode, response::IntoResponse, Json};
|
||||
use log::error;
|
||||
use serde::Serialize;
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum Error {
|
||||
#[error("internal server error")]
|
||||
InternalServerError,
|
||||
|
||||
#[error("bad request")]
|
||||
BadRequest,
|
||||
|
||||
#[error("csrf check failed")]
|
||||
CsrfCheckFailed,
|
||||
|
||||
#[error("sqlx error: {0:?}")]
|
||||
Sqlx(#[from] sqlx::Error),
|
||||
|
||||
#[error("base64 error: {0:?}")]
|
||||
Base64User(base64::DecodeError),
|
||||
}
|
||||
|
||||
impl IntoResponse for Error {
|
||||
fn into_response(self) -> axum::response::Response {
|
||||
error!("{:?}", self);
|
||||
match self {
|
||||
Self::CsrfCheckFailed => (
|
||||
StatusCode::PRECONDITION_FAILED,
|
||||
Json(JsonError {
|
||||
message: "CSRF check failed".into(),
|
||||
}),
|
||||
)
|
||||
.into_response(),
|
||||
Self::Sqlx(_) => StatusCode::INTERNAL_SERVER_ERROR.into_response(),
|
||||
Self::InternalServerError => StatusCode::INTERNAL_SERVER_ERROR.into_response(),
|
||||
Self::Base64User(_) => StatusCode::BAD_REQUEST.into_response(),
|
||||
Self::BadRequest => StatusCode::BAD_REQUEST.into_response(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct JsonError {
|
||||
message: Box<str>,
|
||||
}
|
13
src/fs.rs
Normal file
13
src/fs.rs
Normal file
|
@ -0,0 +1,13 @@
|
|||
use std::path::PathBuf;
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct FsUser(pub u32);
|
||||
|
||||
impl FsUser {
|
||||
pub fn home_dir(&self) -> PathBuf {
|
||||
PathBuf::from(format!("./data/files/{}", self.0))
|
||||
}
|
||||
pub fn upload_dir(&self) -> PathBuf {
|
||||
PathBuf::from(format!("./data/upload/{}", self.0))
|
||||
}
|
||||
}
|
418
src/main.rs
Normal file
418
src/main.rs
Normal file
|
@ -0,0 +1,418 @@
|
|||
#![feature(btree_cursors)]
|
||||
|
||||
use std::{env, path::PathBuf, sync::Arc};
|
||||
|
||||
use axum::{
|
||||
async_trait,
|
||||
body::Body,
|
||||
debug_handler,
|
||||
error_handling::HandleErrorLayer,
|
||||
extract::{FromRequestParts, Path, Query, Request, State},
|
||||
http::{
|
||||
header::{AUTHORIZATION, CONTENT_TYPE, USER_AGENT},
|
||||
request::Parts,
|
||||
uri::PathAndQuery,
|
||||
HeaderMap, StatusCode, Uri,
|
||||
},
|
||||
response::{IntoResponse, Redirect, Response},
|
||||
routing::{any, get, head},
|
||||
BoxError, Json, Router,
|
||||
};
|
||||
use axum_oidc::{
|
||||
error::MiddlewareError, EmptyAdditionalClaims, OidcAuthLayer, OidcClaims, OidcLoginLayer,
|
||||
};
|
||||
use base64::{engine::general_purpose::STANDARD as base64std, Engine};
|
||||
use bytes::Bytes;
|
||||
use dav_fs::FilesFs;
|
||||
use dotenvy::dotenv;
|
||||
use fs::FsUser;
|
||||
use log::debug;
|
||||
use ocs::{OcsJson, OcsXml};
|
||||
use rand::{distributions, Rng};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sha3::{Digest, Sha3_256};
|
||||
use sqlx::{query, query_as, FromRow, MySqlPool};
|
||||
use tokio::{fs::File, io::AsyncReadExt, net::TcpListener};
|
||||
use tower::ServiceBuilder;
|
||||
use tower_http::trace::TraceLayer;
|
||||
use tower_sessions::{cookie::SameSite, MemoryStore, SessionManagerLayer};
|
||||
use webdav_handler::{memfs::MemFs, memls::MemLs, DavConfig, DavHandler};
|
||||
|
||||
use crate::error::Error;
|
||||
|
||||
mod dav_fs;
|
||||
mod error;
|
||||
mod fs;
|
||||
mod ocs;
|
||||
mod upload_fs;
|
||||
|
||||
type HResult<T> = Result<T, Error>;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct AppState {
|
||||
dav_server: Arc<DavHandler>,
|
||||
db: MySqlPool,
|
||||
share_password_salt: Arc<str>,
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
dotenv().ok();
|
||||
env_logger::init();
|
||||
|
||||
let database_url = env::var("DATABASE_URL").expect("DATABASE_URL");
|
||||
|
||||
let database_pool = MySqlPool::connect(&database_url)
|
||||
.await
|
||||
.expect("working database connection");
|
||||
|
||||
let application_base = env::var("APPLICATION_BASE").expect("APPLICATION_BASE");
|
||||
let application_base =
|
||||
Uri::from_maybe_shared(application_base).expect("valid APPLICATION_BASE");
|
||||
let issuer = env::var("ISSUER").expect("ISSUER");
|
||||
let client_id = env::var("CLIENT_ID").expect("CLIENT_ID");
|
||||
let client_secret = env::var("CLIENT_SECRET").ok();
|
||||
let scopes: Vec<String> = env::var("SCOPES")
|
||||
.expect("SCOPES")
|
||||
.split(' ')
|
||||
.map(String::from)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let session_store = MemoryStore::default();
|
||||
let session_service = ServiceBuilder::new()
|
||||
.layer(HandleErrorLayer::new(|_: BoxError| async {
|
||||
StatusCode::BAD_REQUEST
|
||||
}))
|
||||
.layer(SessionManagerLayer::new(session_store).with_same_site(SameSite::Lax));
|
||||
|
||||
let oidc_login_service = ServiceBuilder::new()
|
||||
.layer(HandleErrorLayer::new(|e: MiddlewareError| async {
|
||||
debug!("auth layer error {:?}", e);
|
||||
e.into_response(); //TODO return this response
|
||||
StatusCode::INTERNAL_SERVER_ERROR
|
||||
}))
|
||||
.layer(OidcLoginLayer::<EmptyAdditionalClaims>::new());
|
||||
|
||||
let oidc_auth_service = ServiceBuilder::new()
|
||||
.layer(HandleErrorLayer::new(|e: MiddlewareError| async {
|
||||
debug!("auth layer error {:?}", e);
|
||||
e.into_response(); //TODO return this response
|
||||
StatusCode::INTERNAL_SERVER_ERROR
|
||||
}))
|
||||
.layer(
|
||||
OidcAuthLayer::<EmptyAdditionalClaims>::discover_client(
|
||||
application_base,
|
||||
issuer,
|
||||
client_id,
|
||||
client_secret,
|
||||
scopes,
|
||||
)
|
||||
.await
|
||||
.unwrap(),
|
||||
);
|
||||
|
||||
let state = AppState {
|
||||
dav_server: Arc::new(
|
||||
DavHandler::builder()
|
||||
.filesystem(MemFs::new())
|
||||
.locksystem(MemLs::new())
|
||||
.build_handler(),
|
||||
),
|
||||
db: database_pool,
|
||||
share_password_salt: "".into(),
|
||||
};
|
||||
|
||||
let app = Router::new()
|
||||
.route("/index.php/login/flow", get(login_flow))
|
||||
.layer(oidc_login_service)
|
||||
.route("/index.php/204", get(connectivity_check))
|
||||
.route("/status.php", get(status))
|
||||
//.route("/remote.php/dav", any(remote_dav))
|
||||
.route(
|
||||
"/remote.php/dav",
|
||||
head(|| async { StatusCode::OK.into_response() }),
|
||||
)
|
||||
.nest_service(
|
||||
"/remote.php/dav/files/:user_id",
|
||||
any(user_dav).with_state(state.clone()),
|
||||
)
|
||||
.nest_service(
|
||||
"/remote.php/dav/upload/:user_id",
|
||||
any(upload_dav).with_state(state.clone()),
|
||||
)
|
||||
.nest("/ocs/v2.php", ocs::router(state.clone()))
|
||||
.route("/index.php/avatar/:user_id/512", get(avatar_512))
|
||||
.route("/index.php/avatar/:user_id/:size", get(|| async { "" }))
|
||||
.layer(oidc_auth_service)
|
||||
.layer(session_service)
|
||||
.layer(TraceLayer::new_for_http())
|
||||
.with_state(state);
|
||||
|
||||
let listener = TcpListener::bind("[::]:8080").await.expect("valid address");
|
||||
axum::serve(listener, app).await.unwrap()
|
||||
}
|
||||
|
||||
async fn connectivity_check() -> impl IntoResponse {
|
||||
StatusCode::NO_CONTENT
|
||||
}
|
||||
|
||||
async fn avatar_512() -> Response {
|
||||
let mut buf = vec![];
|
||||
let mut image_file = File::open("static/ferris.png").await.unwrap();
|
||||
image_file.read_to_end(&mut buf).await;
|
||||
let buf = Bytes::from(buf);
|
||||
Response::builder()
|
||||
.header(CONTENT_TYPE, "image/png")
|
||||
.body(Body::from(buf))
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct Format {
|
||||
format: Option<String>,
|
||||
}
|
||||
|
||||
async fn ocs_default(Query(query): Query<Format>) -> Response {
|
||||
if query.format.as_deref() == Some("json") {
|
||||
OcsJson::not_implemented(()).into_response()
|
||||
} else {
|
||||
OcsXml::not_implemented(()).into_response()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct Status {
|
||||
installed: bool,
|
||||
maintenance: bool,
|
||||
needs_db_upgrade: bool,
|
||||
version: Box<str>,
|
||||
version_string: Box<str>,
|
||||
edition: Box<str>,
|
||||
productname: Box<str>,
|
||||
extended_support: bool,
|
||||
}
|
||||
|
||||
async fn status() -> Json<Status> {
|
||||
debug!("GET /status.php");
|
||||
Json(Status {
|
||||
installed: true,
|
||||
maintenance: false,
|
||||
needs_db_upgrade: false,
|
||||
version: "27.1.3".into(),
|
||||
version_string: "27.1.3".into(),
|
||||
edition: "".into(),
|
||||
productname: "pfzettos Nextcloud Backend".into(),
|
||||
extended_support: false,
|
||||
})
|
||||
}
|
||||
|
||||
async fn user_dav(
|
||||
State(state): State<AppState>,
|
||||
user: User,
|
||||
Path(user_id): Path<u32>,
|
||||
mut req: Request,
|
||||
) -> Response {
|
||||
if user_id != user.id {
|
||||
return StatusCode::UNAUTHORIZED.into_response();
|
||||
}
|
||||
|
||||
let user_prefix = format!("/remote.php/dav/files/{}", user.id);
|
||||
|
||||
let path = match req.uri().path_and_query() {
|
||||
Some(x) => format!(
|
||||
"{}{}?{}",
|
||||
user_prefix,
|
||||
x.path(),
|
||||
x.query().unwrap_or_default()
|
||||
),
|
||||
None => format!("{}{}", user_prefix, req.uri().path()),
|
||||
}
|
||||
.parse()
|
||||
.unwrap();
|
||||
|
||||
let mut parts = req.uri().clone().into_parts();
|
||||
parts.path_and_query = Some(path);
|
||||
*req.uri_mut() = Uri::from_parts(parts).unwrap();
|
||||
|
||||
let dav_config = DavConfig::new()
|
||||
.strip_prefix(user_prefix)
|
||||
.autoindex(true, None)
|
||||
.filesystem(Box::new(FilesFs {
|
||||
user: FsUser(user.id),
|
||||
db: state.db.clone(),
|
||||
incoming_share_cache: Arc::default(),
|
||||
}));
|
||||
|
||||
state
|
||||
.dav_server
|
||||
.handle_with(dav_config, req)
|
||||
.await
|
||||
.into_response()
|
||||
}
|
||||
|
||||
async fn upload_dav(
|
||||
State(state): State<AppState>,
|
||||
user: User,
|
||||
Path(user_id): Path<u32>,
|
||||
req: Request,
|
||||
) -> Response {
|
||||
if user_id != user.id {
|
||||
return StatusCode::UNAUTHORIZED.into_response();
|
||||
}
|
||||
|
||||
let dav_config = DavConfig::new()
|
||||
.autoindex(true, None)
|
||||
.filesystem(MemFs::new());
|
||||
|
||||
state
|
||||
.dav_server
|
||||
.handle_with(dav_config, req)
|
||||
.await
|
||||
.into_response()
|
||||
}
|
||||
|
||||
async fn login_flow(
|
||||
State(state): State<AppState>,
|
||||
headers: HeaderMap,
|
||||
OidcClaims(claims): OidcClaims<EmptyAdditionalClaims>,
|
||||
) -> HResult<Redirect> {
|
||||
let user_id = query!(
|
||||
"SELECT id FROM users WHERE oidc_id = ?",
|
||||
claims.subject().to_string()
|
||||
)
|
||||
.fetch_optional(&state.db)
|
||||
.await?;
|
||||
|
||||
let user_id = match user_id {
|
||||
Some(row) => row.id,
|
||||
None => {
|
||||
let mut transaction = state.db.begin().await?;
|
||||
query!(
|
||||
"INSERT INTO users (oidc_id, name) VALUES (?, ?)",
|
||||
claims.subject().to_string(),
|
||||
claims
|
||||
.preferred_username()
|
||||
.map(|x| x.to_string())
|
||||
.unwrap_or_default()
|
||||
)
|
||||
.execute(&mut *transaction)
|
||||
.await?;
|
||||
|
||||
let id = query!("SELECT LAST_INSERT_ID() AS id")
|
||||
.fetch_one(&mut *transaction)
|
||||
.await?
|
||||
.id as u32;
|
||||
transaction.commit().await?;
|
||||
id
|
||||
}
|
||||
};
|
||||
|
||||
let user_token = rand::thread_rng()
|
||||
.sample_iter(distributions::Alphanumeric)
|
||||
.take(64)
|
||||
.map(char::from)
|
||||
.collect::<String>();
|
||||
let user_token_hash = {
|
||||
let mut token_hasher = Sha3_256::default();
|
||||
token_hasher.update(&user_token);
|
||||
token_hasher.finalize().to_vec()
|
||||
};
|
||||
|
||||
let user_agent = headers
|
||||
.get(USER_AGENT)
|
||||
.and_then(|x| x.to_str().ok())
|
||||
.unwrap_or("");
|
||||
|
||||
query!(
|
||||
"INSERT INTO user_tokens (user_id, name, hash) VALUES (?, ?, ?)",
|
||||
user_id,
|
||||
user_agent,
|
||||
user_token_hash
|
||||
)
|
||||
.execute(&state.db)
|
||||
.await?;
|
||||
|
||||
let mut transaction = state.db.begin().await?;
|
||||
query!(
|
||||
"INSERT INTO user_tokens (user_id, name, hash) VALUES (?, ?, ?)",
|
||||
user_id,
|
||||
user_agent,
|
||||
user_token_hash
|
||||
)
|
||||
.execute(&mut *transaction)
|
||||
.await?;
|
||||
|
||||
let token_id = query!("SELECT LAST_INSERT_ID() AS id")
|
||||
.fetch_one(&mut *transaction)
|
||||
.await?
|
||||
.id as u32;
|
||||
transaction.commit().await?;
|
||||
|
||||
Ok(Redirect::temporary(&format!(
|
||||
"nc://login/server:http://10.50.10.2:8080&user:{}&password:{}",
|
||||
token_id, user_token
|
||||
)))
|
||||
}
|
||||
|
||||
impl AsRef<MySqlPool> for AppState {
|
||||
fn as_ref(&self) -> &MySqlPool {
|
||||
&self.db
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(FromRow, Clone)]
|
||||
pub struct User {
|
||||
id: u32,
|
||||
oidc_id: Box<str>,
|
||||
name: Box<str>,
|
||||
}
|
||||
|
||||
impl User {
|
||||
pub fn home_dir(&self) -> PathBuf {
|
||||
PathBuf::from(format!("./files/{}/", self.id))
|
||||
}
|
||||
pub fn upload_dir(&self) -> PathBuf {
|
||||
PathBuf::from(format!("./upload/{}/", self.id))
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl<S: AsRef<MySqlPool> + Send + Sync> FromRequestParts<S> for User {
|
||||
type Rejection = Result<StatusCode, Error>;
|
||||
|
||||
async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
|
||||
let headers = HeaderMap::from_request_parts(parts, state)
|
||||
.await
|
||||
.map_err(|_| Err(Error::InternalServerError))?;
|
||||
let authorizazion = headers
|
||||
.get(AUTHORIZATION)
|
||||
.ok_or(Ok(StatusCode::UNAUTHORIZED))?;
|
||||
if let Some(basic) = authorizazion
|
||||
.to_str()
|
||||
.unwrap_or_default()
|
||||
.strip_prefix("Basic ")
|
||||
{
|
||||
let basic = base64std
|
||||
.decode(basic)
|
||||
.map_err(|e| Err(Error::Base64User(e)))?;
|
||||
let basic = std::str::from_utf8(&basic).unwrap_or_default();
|
||||
let (username, password) = basic.split_once(':').unwrap_or_default();
|
||||
|
||||
let password_hash = {
|
||||
let mut hasher = Sha3_256::default();
|
||||
hasher.update(password);
|
||||
hasher.finalize().to_vec()
|
||||
};
|
||||
|
||||
let user = query_as!(User, "SELECT users.id, users.oidc_id, users.name FROM user_tokens INNER JOIN users ON user_tokens.user_id=users.id WHERE user_tokens.id = ? AND user_tokens.hash = ?", username, password_hash).fetch_optional(state.as_ref()).await.map_err(|x|Err(x.into()))?;
|
||||
|
||||
match user {
|
||||
Some(user) => Ok(user),
|
||||
None => Err(Ok(StatusCode::UNAUTHORIZED)),
|
||||
}
|
||||
} else {
|
||||
Err(Ok(StatusCode::UNAUTHORIZED))
|
||||
}
|
||||
}
|
||||
}
|
159
src/ocs.rs
Normal file
159
src/ocs.rs
Normal file
|
@ -0,0 +1,159 @@
|
|||
use axum::{
|
||||
http::header::CONTENT_TYPE,
|
||||
response::{IntoResponse, Response},
|
||||
routing::{get, post},
|
||||
Router,
|
||||
};
|
||||
use serde::Serialize;
|
||||
|
||||
use crate::AppState;
|
||||
|
||||
pub mod core;
|
||||
pub mod files_sharing;
|
||||
pub mod provisioning_api;
|
||||
|
||||
pub fn router(state: AppState) -> Router<AppState> {
|
||||
Router::new()
|
||||
.route("/cloud/capabilities", get(core::get_status))
|
||||
.route("/cloud/users", get(provisioning_api::get_users))
|
||||
.route("/cloud/user", get(provisioning_api::get_user))
|
||||
.route(
|
||||
"/apps/files_sharing/api/v1/shares",
|
||||
get(files_sharing::share::get_shares).post(files_sharing::share::create),
|
||||
)
|
||||
.route(
|
||||
"/apps/files_sharing/api/v1/sharees",
|
||||
get(files_sharing::sharees::search),
|
||||
)
|
||||
.with_state(state)
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct OcsJson<T: Serialize> {
|
||||
ocs: Ocs<T>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct OcsXml<T: Serialize>(Ocs<T>);
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(rename = "ocs")]
|
||||
pub struct Ocs<T: Serialize> {
|
||||
pub meta: OcsMeta,
|
||||
pub data: T,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct OcsMeta {
|
||||
pub status: Box<str>,
|
||||
pub statuscode: u16,
|
||||
pub message: Box<str>,
|
||||
pub totalitems: Option<Box<str>>,
|
||||
pub itemsperpage: Option<Box<str>>,
|
||||
}
|
||||
|
||||
impl<T: Serialize> OcsJson<T> {
|
||||
pub fn ok(data: T) -> Self {
|
||||
Self {
|
||||
ocs: Ocs {
|
||||
meta: OcsMeta {
|
||||
status: "ok".into(),
|
||||
statuscode: 200,
|
||||
message: "ok".into(),
|
||||
totalitems: None,
|
||||
itemsperpage: None,
|
||||
},
|
||||
data,
|
||||
},
|
||||
}
|
||||
}
|
||||
pub fn not_found(data: T) -> Self {
|
||||
Self {
|
||||
ocs: Ocs {
|
||||
meta: OcsMeta {
|
||||
status: "not found".into(),
|
||||
statuscode: 200,
|
||||
message: "not found".into(),
|
||||
totalitems: None,
|
||||
itemsperpage: None,
|
||||
},
|
||||
data,
|
||||
},
|
||||
}
|
||||
}
|
||||
pub fn not_implemented(data: T) -> Self {
|
||||
Self {
|
||||
ocs: Ocs {
|
||||
meta: OcsMeta {
|
||||
status: "not found".into(),
|
||||
statuscode: 200,
|
||||
message: "not found".into(),
|
||||
totalitems: None,
|
||||
itemsperpage: None,
|
||||
},
|
||||
data,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Serialize> OcsXml<T> {
|
||||
pub fn ok(data: T) -> Self {
|
||||
Self(Ocs {
|
||||
meta: OcsMeta {
|
||||
status: "ok".into(),
|
||||
statuscode: 200,
|
||||
message: "ok".into(),
|
||||
totalitems: None,
|
||||
itemsperpage: None,
|
||||
},
|
||||
data,
|
||||
})
|
||||
}
|
||||
pub fn not_found(data: T) -> Self {
|
||||
Self(Ocs {
|
||||
meta: OcsMeta {
|
||||
status: "not found".into(),
|
||||
statuscode: 200,
|
||||
message: "not found".into(),
|
||||
totalitems: None,
|
||||
itemsperpage: None,
|
||||
},
|
||||
data,
|
||||
})
|
||||
}
|
||||
pub fn not_implemented(data: T) -> Self {
|
||||
Self(Ocs {
|
||||
meta: OcsMeta {
|
||||
status: "not implemented".into(),
|
||||
statuscode: 501,
|
||||
message: "not implemented".into(),
|
||||
totalitems: None,
|
||||
itemsperpage: None,
|
||||
},
|
||||
data,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Serialize> IntoResponse for OcsJson<T> {
|
||||
fn into_response(self) -> axum::response::Response {
|
||||
let body = serde_json::to_string(&self).unwrap();
|
||||
Response::builder()
|
||||
.header(CONTENT_TYPE, "application/json")
|
||||
.body(body)
|
||||
.unwrap()
|
||||
.into_response()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Serialize> IntoResponse for OcsXml<T> {
|
||||
fn into_response(self) -> axum::response::Response {
|
||||
let body = serde_xml_rs::to_string(&self).unwrap();
|
||||
Response::builder()
|
||||
.header(CONTENT_TYPE, "application/xml; charset=utf-8")
|
||||
.body(body)
|
||||
.unwrap()
|
||||
.into_response()
|
||||
}
|
||||
}
|
192
src/ocs/core.rs
Normal file
192
src/ocs/core.rs
Normal file
|
@ -0,0 +1,192 @@
|
|||
use axum::http::HeaderMap;
|
||||
use serde::Serialize;
|
||||
|
||||
use crate::{error::Error, HResult};
|
||||
|
||||
use super::OcsJson;
|
||||
|
||||
// https://docs.nextcloud.com/server/latest/developer_manual/_static/openapi.html#/operations/core-ocs-get-capabilities
|
||||
pub async fn get_status(headers: HeaderMap) -> HResult<OcsJson<CapabilitiesData>> {
|
||||
if let Some(Ok("true")) = headers.get("OCS-ApiRequest").map(|x| x.to_str()) {
|
||||
Ok(OcsJson::ok(CapabilitiesData {
|
||||
version: CapabilitiesVersion {
|
||||
major: 0,
|
||||
minor: 1,
|
||||
micro: 0,
|
||||
string: "0.1.0".into(),
|
||||
edition: "".into(),
|
||||
extended_support: false,
|
||||
},
|
||||
capabilities: Capabilities {
|
||||
dav: DavCapabilities {
|
||||
bulkupload: "1.0".into(),
|
||||
chunking: "1.0".into(),
|
||||
},
|
||||
user_status: UserStatusCapabilities::default(),
|
||||
weather_status: WeatherStatusCapabilities::default(),
|
||||
files: FilesCapabilities::default(),
|
||||
files_sharing: FilesSharingCapabilities {
|
||||
api_enabled: true,
|
||||
default_permissions: 31,
|
||||
group_sharing: false,
|
||||
public: FilesSharingPublicCapabilities {
|
||||
enabled: true,
|
||||
multiple_links: true,
|
||||
send_mail: false,
|
||||
upload: true,
|
||||
upload_files_drop: true,
|
||||
..Default::default()
|
||||
},
|
||||
resharing: true,
|
||||
..Default::default()
|
||||
},
|
||||
},
|
||||
}))
|
||||
} else {
|
||||
Err(Error::CsrfCheckFailed)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct CapabilitiesData {
|
||||
version: CapabilitiesVersion,
|
||||
capabilities: Capabilities,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CapabilitiesVersion {
|
||||
major: i64,
|
||||
minor: i64,
|
||||
micro: i64,
|
||||
string: Box<str>,
|
||||
edition: Box<str>,
|
||||
extended_support: bool,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct Capabilities {
|
||||
dav: DavCapabilities,
|
||||
user_status: UserStatusCapabilities,
|
||||
weather_status: WeatherStatusCapabilities,
|
||||
files: FilesCapabilities,
|
||||
files_sharing: FilesSharingCapabilities,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct DavCapabilities {
|
||||
bulkupload: Box<str>,
|
||||
chunking: Box<str>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Default)]
|
||||
pub struct UserStatusCapabilities {
|
||||
enabled: bool,
|
||||
restore: bool,
|
||||
supports_emoji: bool,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Default)]
|
||||
pub struct WeatherStatusCapabilities {
|
||||
enabled: bool,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct FilesCapabilities {
|
||||
bigfilechunking: bool,
|
||||
blacklisted_files: Box<[Box<str>]>,
|
||||
comments: bool,
|
||||
undelete: bool,
|
||||
version_deletion: bool,
|
||||
version_labeling: bool,
|
||||
versioning: bool,
|
||||
}
|
||||
|
||||
impl Default for FilesCapabilities {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
bigfilechunking: true,
|
||||
blacklisted_files: Default::default(),
|
||||
comments: false,
|
||||
undelete: false,
|
||||
version_deletion: false,
|
||||
version_labeling: false,
|
||||
versioning: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Default)]
|
||||
pub struct FilesSharingCapabilities {
|
||||
api_enabled: bool,
|
||||
default_permissions: u32,
|
||||
federation: FilesSharingFederationCapabilities,
|
||||
group_sharing: bool,
|
||||
public: FilesSharingPublicCapabilities,
|
||||
resharing: bool,
|
||||
sharebymail: FilesSharingSharebymailCapabilities,
|
||||
sharee: FilesSharingShareeCapabilities,
|
||||
user: FilesSharingUserCapabilities,
|
||||
}
|
||||
#[derive(Serialize, Default)]
|
||||
pub struct FilesSharingFederationCapabilities {
|
||||
expire_date: Expire,
|
||||
expire_date_supported: Expire,
|
||||
incoming: bool,
|
||||
outgoing: bool,
|
||||
}
|
||||
#[derive(Serialize, Default)]
|
||||
pub struct FilesSharingPublicCapabilities {
|
||||
enabled: bool,
|
||||
expire_date: Expire,
|
||||
expire_date_internal: Expire,
|
||||
expire_date_remote: Expire,
|
||||
|
||||
multiple_links: bool,
|
||||
|
||||
send_mail: bool,
|
||||
upload: bool,
|
||||
upload_files_drop: bool,
|
||||
}
|
||||
#[derive(Serialize, Default)]
|
||||
pub struct FilesSharingSharebymailCapabilities {
|
||||
enabled: bool,
|
||||
expire_date: Expire,
|
||||
send_passowrd_by_mail: bool,
|
||||
}
|
||||
#[derive(Serialize, Default)]
|
||||
pub struct FilesSharingUserCapabilities {
|
||||
expire_date: Expire,
|
||||
send_mail: bool,
|
||||
}
|
||||
#[derive(Serialize, Default)]
|
||||
pub struct FilesSharingShareeCapabilities {
|
||||
always_show_unique: bool,
|
||||
query_lookup_default: bool,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Default)]
|
||||
pub struct Expire {
|
||||
enabled: bool,
|
||||
}
|
||||
|
||||
/*
|
||||
//TODO: doesn't work
|
||||
pub async fn get_avatar(Path((user_id, size)): Path<(u32, u32)>) -> impl IntoResponse {
|
||||
let image = Reader::open("static/ferris.png").unwrap().decode().unwrap();
|
||||
image.resize(size, size, FilterType::Nearest);
|
||||
let mut buf = vec![];
|
||||
image.write_to(&mut Cursor::new(&mut buf), ImageFormat::Png);
|
||||
|
||||
Response::builder()
|
||||
.status(StatusCode::OK)
|
||||
.header(CONTENT_TYPE, "image/png")
|
||||
.header(
|
||||
CONTENT_DISPOSITION,
|
||||
format!("inline; filename=\"avatar.{size}.png\""),
|
||||
)
|
||||
.header("X-NC-IsCustomAvatar", "1")
|
||||
.body(Body::from(buf))
|
||||
.unwrap()
|
||||
}
|
||||
*/
|
187
src/ocs/files_sharing.rs
Normal file
187
src/ocs/files_sharing.rs
Normal file
|
@ -0,0 +1,187 @@
|
|||
use serde::{Deserialize, Serialize};
|
||||
|
||||
pub mod share;
|
||||
pub mod sharees;
|
||||
|
||||
#[derive(Debug, Default, PartialEq, Eq)]
|
||||
pub enum ShareType {
|
||||
#[default]
|
||||
Unknown,
|
||||
User,
|
||||
Group,
|
||||
Usergroup,
|
||||
Link,
|
||||
Email,
|
||||
#[deprecated]
|
||||
Contact,
|
||||
Remote,
|
||||
Circle,
|
||||
Guest,
|
||||
RemoteGroup,
|
||||
Room,
|
||||
UserRoom,
|
||||
Deck,
|
||||
DeckUser,
|
||||
Sciencemesh,
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for ShareType {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
let value = i32::deserialize(deserializer)?;
|
||||
Ok(match value {
|
||||
0 => Self::User,
|
||||
1 => Self::Group,
|
||||
2 => Self::Usergroup,
|
||||
3 => Self::Link,
|
||||
4 => Self::Email,
|
||||
5 => Self::Contact,
|
||||
6 => Self::Remote,
|
||||
7 => Self::Circle,
|
||||
8 => Self::Guest,
|
||||
9 => Self::RemoteGroup,
|
||||
10 => Self::Room,
|
||||
11 => Self::UserRoom,
|
||||
12 => Self::Deck,
|
||||
13 => Self::DeckUser,
|
||||
15 => Self::Sciencemesh,
|
||||
_ => Self::Unknown,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Serialize for ShareType {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: serde::Serializer,
|
||||
{
|
||||
let value = match self {
|
||||
Self::Unknown => -1,
|
||||
Self::User => 0,
|
||||
Self::Group => 1,
|
||||
Self::Usergroup => 2,
|
||||
Self::Link => 3,
|
||||
Self::Email => 4,
|
||||
Self::Contact => 5,
|
||||
Self::Remote => 6,
|
||||
Self::Circle => 7,
|
||||
Self::Guest => 8,
|
||||
Self::RemoteGroup => 9,
|
||||
Self::Room => 10,
|
||||
Self::UserRoom => 11,
|
||||
Self::Deck => 12,
|
||||
Self::DeckUser => 13,
|
||||
Self::Sciencemesh => 15,
|
||||
};
|
||||
value.serialize(serializer)
|
||||
}
|
||||
}
|
||||
|
||||
impl ShareType {
|
||||
pub fn all() -> Box<[Self]> {
|
||||
vec![
|
||||
Self::User,
|
||||
Self::Group,
|
||||
Self::Usergroup,
|
||||
Self::Link,
|
||||
Self::Email,
|
||||
Self::Remote,
|
||||
Self::Circle,
|
||||
Self::Guest,
|
||||
Self::RemoteGroup,
|
||||
Self::Room,
|
||||
Self::UserRoom,
|
||||
Self::Deck,
|
||||
Self::DeckUser,
|
||||
Self::Sciencemesh,
|
||||
]
|
||||
.into()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone)]
|
||||
pub enum SharePermission {
|
||||
Read = 1,
|
||||
Update = 2,
|
||||
Create = 4,
|
||||
Delete = 8,
|
||||
Share = 16,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone)]
|
||||
pub struct SharePermissions(pub Box<[SharePermission]>);
|
||||
|
||||
impl SharePermissions {
|
||||
pub fn rus() -> Self {
|
||||
Self(
|
||||
vec![
|
||||
SharePermission::Read,
|
||||
SharePermission::Update,
|
||||
SharePermission::Share,
|
||||
]
|
||||
.into(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<u8> for SharePermissions {
|
||||
fn from(value: u8) -> Self {
|
||||
let mut permissions = vec![];
|
||||
if value & 1 == 1 {
|
||||
permissions.push(SharePermission::Read);
|
||||
}
|
||||
if value >> 1 & 1 == 1 {
|
||||
permissions.push(SharePermission::Update);
|
||||
}
|
||||
if value >> 2 & 1 == 1 {
|
||||
permissions.push(SharePermission::Create);
|
||||
}
|
||||
if value >> 3 & 1 == 1 {
|
||||
permissions.push(SharePermission::Delete);
|
||||
}
|
||||
if value >> 4 & 1 == 1 {
|
||||
permissions.push(SharePermission::Share);
|
||||
}
|
||||
|
||||
Self(permissions.into())
|
||||
}
|
||||
}
|
||||
impl From<&SharePermissions> for u8 {
|
||||
fn from(value: &SharePermissions) -> Self {
|
||||
let mut out = 0;
|
||||
for p in value.0.iter() {
|
||||
match p {
|
||||
SharePermission::Read => out += 1,
|
||||
SharePermission::Update => out += 2,
|
||||
SharePermission::Create => out += 4,
|
||||
SharePermission::Delete => out += 8,
|
||||
SharePermission::Share => out += 16,
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for SharePermissions {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
let value = u8::deserialize(deserializer)
|
||||
.map(|x| x.into())
|
||||
.unwrap_or_else(|_| Self::rus());
|
||||
|
||||
Ok(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl Serialize for SharePermissions {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: serde::Serializer,
|
||||
{
|
||||
u8::from(self).serialize(serializer)
|
||||
}
|
||||
}
|
154
src/ocs/files_sharing/share.rs
Normal file
154
src/ocs/files_sharing/share.rs
Normal file
|
@ -0,0 +1,154 @@
|
|||
use axum::{
|
||||
extract::State,
|
||||
response::{IntoResponse, Response},
|
||||
Form,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sqlx::query;
|
||||
use time::{macros::format_description, Time};
|
||||
|
||||
use crate::{error::Error, ocs::OcsXml, AppState, HResult, User};
|
||||
|
||||
use super::{SharePermission, SharePermissions, ShareType};
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
pub struct CreateShareData {
|
||||
attributes: Option<Box<str>>,
|
||||
#[serde(default, rename = "expireDate")]
|
||||
expire_date: Option<Box<str>>,
|
||||
#[serde(default)]
|
||||
label: Box<str>,
|
||||
#[serde(default)]
|
||||
note: Box<str>,
|
||||
#[serde(default)]
|
||||
password: Box<str>,
|
||||
path: Option<Box<str>>,
|
||||
#[serde(default = "SharePermissions::rus")]
|
||||
permissions: SharePermissions,
|
||||
#[serde(default, rename = "publicUpload")]
|
||||
public_upload: bool,
|
||||
#[serde(default, rename = "shareType")]
|
||||
share_type: ShareType,
|
||||
#[serde(rename = "shareWith")]
|
||||
share_with: Option<Box<str>>,
|
||||
}
|
||||
|
||||
pub async fn create(
|
||||
State(state): State<AppState>,
|
||||
user: User,
|
||||
Form(form): Form<CreateShareData>,
|
||||
) -> HResult<Response> {
|
||||
match form.share_type {
|
||||
ShareType::User => Ok(create_user_share(&state, &user, &form)
|
||||
.await?
|
||||
.into_response()),
|
||||
_ => todo!(),
|
||||
}
|
||||
}
|
||||
|
||||
async fn create_user_share(
|
||||
state: &AppState,
|
||||
user: &User,
|
||||
data: &CreateShareData,
|
||||
) -> HResult<OcsXml<UserShare>> {
|
||||
// shareWith is required
|
||||
let share_with = match &data.share_with {
|
||||
Some(x) => x.as_ref(),
|
||||
None => return Err(Error::BadRequest),
|
||||
};
|
||||
|
||||
// check if user_to is valid
|
||||
if query!("SELECT id FROM users WHERE id = ?", share_with)
|
||||
.fetch_optional(&state.db)
|
||||
.await?
|
||||
.is_none()
|
||||
{
|
||||
return Err(Error::BadRequest);
|
||||
}
|
||||
|
||||
//TODO: parse expire_date
|
||||
let expires_at: Option<Time> = None;
|
||||
|
||||
//TODO: change to owner of orig if resharing
|
||||
let dst_user_id = user.id;
|
||||
let dst_user_name = user.name.clone();
|
||||
|
||||
//TODO: check permissions of user on data.path
|
||||
|
||||
let share_id = {
|
||||
let mut transaction = state.db.begin().await?;
|
||||
//query!("INSERT INTO user_shares (dst_user_id, from_user_id, to_user_id, expires_at, note, path, permissions) VALUES (?, ?, ?, ?, ?, ?, ?)", &dst_user_id, &user.id, &share_with, expires_at, &data.note, &data.path, u8::from(&data.permissions)).execute(&state.db).await?;
|
||||
|
||||
let row = query!("SELECT LAST_INSERT_ID() as id")
|
||||
.fetch_one(&mut *transaction)
|
||||
.await?;
|
||||
transaction.commit().await?;
|
||||
row.id
|
||||
};
|
||||
|
||||
Ok(OcsXml::ok(UserShare {
|
||||
id: share_id,
|
||||
share_type: ShareType::User,
|
||||
uid_owner: user.id,
|
||||
displayname_owner: user.name.clone(),
|
||||
permissions: data.permissions.clone(),
|
||||
can_edit: data.permissions.0.contains(&SharePermission::Update),
|
||||
can_delete: data.permissions.0.contains(&SharePermission::Delete),
|
||||
expiration: expires_at.and_then(|x| {
|
||||
x.format(format_description!(
|
||||
"[year]-[month]-[day] [hour]:[minute]:[second]"
|
||||
))
|
||||
.ok()
|
||||
.map(|x| x.into())
|
||||
}),
|
||||
uid_file_owner: dst_user_id,
|
||||
note: data.note.clone(),
|
||||
label: data.label.clone(),
|
||||
displayname_file_owner: dst_user_name,
|
||||
..Default::default()
|
||||
}))
|
||||
}
|
||||
|
||||
#[derive(Default, Serialize)]
|
||||
struct UserShare {
|
||||
id: u64,
|
||||
share_type: ShareType,
|
||||
uid_owner: u32,
|
||||
displayname_owner: Box<str>,
|
||||
permissions: SharePermissions,
|
||||
can_edit: bool,
|
||||
can_delete: bool,
|
||||
stime: (),
|
||||
parent: Box<str>,
|
||||
expiration: Option<Box<str>>,
|
||||
token: Box<str>,
|
||||
uid_file_owner: u32,
|
||||
note: Box<str>,
|
||||
label: Box<str>,
|
||||
displayname_file_owner: Box<str>,
|
||||
path: Box<str>,
|
||||
item_type: Box<str>,
|
||||
item_permissions: u8,
|
||||
mimetype: Box<str>,
|
||||
has_preview: bool,
|
||||
storage_id: Box<str>,
|
||||
storage: Box<str>,
|
||||
item_source: (),
|
||||
file_source: (),
|
||||
file_parent: (),
|
||||
file_target: (),
|
||||
item_size: (),
|
||||
item_mtime: (),
|
||||
share_with: Box<str>,
|
||||
share_with_displayname: Box<str>,
|
||||
share_with_displayname_unique: Box<str>,
|
||||
status: (),
|
||||
mail_send: bool,
|
||||
hide_download: bool,
|
||||
attributes: (),
|
||||
tags: Box<[()]>,
|
||||
}
|
||||
|
||||
pub async fn get_shares() -> HResult<OcsXml<()>> {
|
||||
Ok(OcsXml::ok(()))
|
||||
}
|
170
src/ocs/files_sharing/sharees.rs
Normal file
170
src/ocs/files_sharing/sharees.rs
Normal file
|
@ -0,0 +1,170 @@
|
|||
use axum::{
|
||||
extract::{Query, State},
|
||||
response::IntoResponse,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sqlx::query;
|
||||
|
||||
use crate::{
|
||||
ocs::{OcsJson, OcsXml},
|
||||
AppState, HResult, User,
|
||||
};
|
||||
|
||||
use super::ShareType;
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct SearchQuery {
|
||||
#[serde(rename = "itemType")]
|
||||
item_type: Option<Box<str>>,
|
||||
|
||||
#[serde(default)]
|
||||
lookup: bool,
|
||||
|
||||
#[serde(default = "default_page")]
|
||||
page: u64,
|
||||
|
||||
#[serde(rename = "perPage", default = "default_per_page")]
|
||||
per_page: u64,
|
||||
|
||||
search: Box<str>,
|
||||
|
||||
#[serde(default = "default_share_types")]
|
||||
share_type: Box<[ShareType]>,
|
||||
}
|
||||
|
||||
fn default_page() -> u64 {
|
||||
1
|
||||
}
|
||||
|
||||
fn default_per_page() -> u64 {
|
||||
200
|
||||
}
|
||||
|
||||
fn default_share_types() -> Box<[ShareType]> {
|
||||
ShareType::all()
|
||||
}
|
||||
|
||||
pub async fn search(
|
||||
State(state): State<AppState>,
|
||||
user: User,
|
||||
Query(query): Query<SearchQuery>,
|
||||
) -> HResult<OcsJson<SearchResult>> {
|
||||
let mut transaction = state.db.begin().await?;
|
||||
let (rough_users, exact_users) = if query.share_type.contains(&ShareType::User) {
|
||||
let rough = query!(
|
||||
"SELECT id, name FROM users WHERE MATCH(name) AGAINST (? IN NATURAL LANGUAGE MODE) AND name != ?",
|
||||
&query.search,
|
||||
&query.search,
|
||||
)
|
||||
.fetch_all(&mut *transaction)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|x| SearchUser {
|
||||
icon: "user-icon".into(),
|
||||
label: x.name.clone().into(),
|
||||
share_with_displayname_unique: x.name.into(),
|
||||
value: SearchUserValue {
|
||||
share_type: ShareType::User,
|
||||
share_with: x.id.to_string().into(),
|
||||
},
|
||||
..Default::default()
|
||||
})
|
||||
.collect();
|
||||
|
||||
let exact = query!("SELECT id, name FROM users WHERE name = ?", &query.search)
|
||||
.fetch_all(&mut *transaction)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|x| SearchUser {
|
||||
icon: "user-icon".into(),
|
||||
label: x.name.clone().into(),
|
||||
share_with_displayname_unique: x.name.into(),
|
||||
value: SearchUserValue {
|
||||
share_type: ShareType::User,
|
||||
share_with: x.id.to_string().into(),
|
||||
},
|
||||
..Default::default()
|
||||
})
|
||||
.collect();
|
||||
(rough, exact)
|
||||
} else {
|
||||
(vec![], vec![])
|
||||
};
|
||||
|
||||
transaction.commit().await?;
|
||||
|
||||
Ok(OcsJson::ok(SearchResult {
|
||||
users: rough_users.into(),
|
||||
exact: ExactSearchResult {
|
||||
users: exact_users.into(),
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
}))
|
||||
}
|
||||
|
||||
#[derive(Serialize, Default)]
|
||||
pub struct SearchResult {
|
||||
circles: Box<[()]>,
|
||||
emails: Box<[()]>,
|
||||
exact: ExactSearchResult,
|
||||
groups: Box<[()]>,
|
||||
lookup: Box<[()]>,
|
||||
#[serde(rename = "lookupEnabled")]
|
||||
lookup_enabled: bool,
|
||||
remote_groups: Box<[()]>,
|
||||
remotes: Box<[()]>,
|
||||
rooms: Box<[()]>,
|
||||
users: Box<[SearchUser]>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Default)]
|
||||
pub struct ExactSearchResult {
|
||||
circles: Box<[()]>,
|
||||
emails: Box<[()]>,
|
||||
groups: Box<[()]>,
|
||||
lookup: Box<[()]>,
|
||||
remote_groups: Box<[()]>,
|
||||
remotes: Box<[()]>,
|
||||
rooms: Box<[()]>,
|
||||
users: Box<[SearchUser]>,
|
||||
}
|
||||
|
||||
#[derive(Default, Serialize)]
|
||||
pub struct SearchUser {
|
||||
icon: Box<str>,
|
||||
label: Box<str>,
|
||||
#[serde(rename = "shareWithDisplayNameUnique")]
|
||||
share_with_displayname_unique: Box<str>,
|
||||
status: SearchUserStatus,
|
||||
subline: Box<str>,
|
||||
value: SearchUserValue,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct SearchUserStatus {
|
||||
#[serde(rename = "clearAt")]
|
||||
clear_at: (),
|
||||
icon: (),
|
||||
message: (),
|
||||
status: Box<str>,
|
||||
}
|
||||
|
||||
impl Default for SearchUserStatus {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
clear_at: Default::default(),
|
||||
icon: Default::default(),
|
||||
message: Default::default(),
|
||||
status: "offline".into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Serialize)]
|
||||
pub struct SearchUserValue {
|
||||
#[serde(rename = "shareType")]
|
||||
share_type: ShareType,
|
||||
#[serde(rename = "shareWith")]
|
||||
share_with: Box<str>,
|
||||
}
|
126
src/ocs/provisioning_api.rs
Normal file
126
src/ocs/provisioning_api.rs
Normal file
|
@ -0,0 +1,126 @@
|
|||
use serde::Serialize;
|
||||
|
||||
use crate::{HResult, User};
|
||||
|
||||
use super::OcsJson;
|
||||
|
||||
pub async fn get_users(user: User) -> HResult<OcsJson<OcsUsers>> {
|
||||
Ok(OcsJson::ok(OcsUsers {
|
||||
users: vec!["demo".into()].into(),
|
||||
}))
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct OcsUsers {
|
||||
users: Box<[Box<str>]>,
|
||||
}
|
||||
|
||||
pub async fn get_user(user: User) -> HResult<OcsJson<OcsUser>> {
|
||||
Ok(OcsJson::ok(OcsUser {
|
||||
id: user.id.to_string().into(),
|
||||
language: "de".into(),
|
||||
enabled: Some(true),
|
||||
profile_enabled: "1".into(),
|
||||
display_name: user.name.clone(),
|
||||
display_name_2: user.name,
|
||||
groups: vec!["demo".into()].into(),
|
||||
last_login: 0,
|
||||
..Default::default()
|
||||
}))
|
||||
}
|
||||
|
||||
#[derive(Serialize, Default)]
|
||||
pub struct OcsUser {
|
||||
additional_mail: Box<[Box<str>]>,
|
||||
#[serde(rename = "additional_mailScope")]
|
||||
additional_mail_scope: Box<[OcsScope]>,
|
||||
address: Box<str>,
|
||||
#[serde(rename = "addressScope")]
|
||||
address_scope: OcsScope,
|
||||
#[serde(rename = "avatarScope")]
|
||||
avatar_scope: OcsScope,
|
||||
backend: Box<str>,
|
||||
#[serde(rename = "backendCapabilities")]
|
||||
backend_capabilities: OcsBackendCapabilities,
|
||||
biography: Box<str>,
|
||||
#[serde(rename = "biographyScope")]
|
||||
biography_scope: OcsScope,
|
||||
#[serde(rename = "display-name")]
|
||||
display_name: Box<str>,
|
||||
#[serde(rename = "displayname")]
|
||||
display_name_2: Box<str>,
|
||||
#[serde(rename = "displaynameScope")]
|
||||
display_name_scope: OcsScope,
|
||||
email: Option<Box<str>>,
|
||||
#[serde(rename = "emailScope")]
|
||||
email_scope: OcsScope,
|
||||
enabled: Option<bool>,
|
||||
fediverse: Box<str>,
|
||||
#[serde(rename = "fediverseScope")]
|
||||
fediverse_scope: OcsScope,
|
||||
groups: Box<[Box<str>]>,
|
||||
headline: Box<str>,
|
||||
#[serde(rename = "headlineScope")]
|
||||
headline_scope: OcsScope,
|
||||
id: Box<str>,
|
||||
language: Box<str>,
|
||||
#[serde(rename = "lastLogin")]
|
||||
last_login: i64,
|
||||
locale: Box<str>,
|
||||
manager: Box<str>,
|
||||
notify_email: Option<Box<str>>,
|
||||
organisation: Box<str>,
|
||||
#[serde(rename = "organisationScope")]
|
||||
organisation_scope: OcsScope,
|
||||
phone: Box<str>,
|
||||
#[serde(rename = "phoneScope")]
|
||||
phone_scope: OcsScope,
|
||||
profile_enabled: Box<str>,
|
||||
#[serde(rename = "profile_enabledScope")]
|
||||
profile_enabled_scope: OcsScope,
|
||||
quota: OcsQuota,
|
||||
role: Box<str>,
|
||||
#[serde(rename = "roleScope")]
|
||||
role_scope: OcsScope,
|
||||
#[serde(rename = "storageLocation")]
|
||||
storage_location: Box<str>,
|
||||
subadmin: Box<[Box<str>]>,
|
||||
twitter: Box<str>,
|
||||
#[serde(rename = "twitterScope")]
|
||||
twitter_scope: OcsScope,
|
||||
website: Box<str>,
|
||||
#[serde(rename = "websiteScope")]
|
||||
website_scope: OcsScope,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Default)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct OcsBackendCapabilities {
|
||||
set_display_name: bool,
|
||||
set_password: bool,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Default)]
|
||||
pub enum OcsScope {
|
||||
#[serde(rename = "v2-private")]
|
||||
Private,
|
||||
|
||||
#[default]
|
||||
#[serde(rename = "v2-local")]
|
||||
Local,
|
||||
|
||||
#[serde(rename = "v2-federated")]
|
||||
Federated,
|
||||
|
||||
#[serde(rename = "v2-published")]
|
||||
Published,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Default)]
|
||||
pub struct OcsQuota {
|
||||
free: u64,
|
||||
quota: i64,
|
||||
relative: f32,
|
||||
total: u64,
|
||||
used: u32,
|
||||
}
|
111
src/upload_fs.rs
Normal file
111
src/upload_fs.rs
Normal file
|
@ -0,0 +1,111 @@
|
|||
use webdav_handler::fs::DavFileSystem;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct UploadFs {}
|
||||
|
||||
impl DavFileSystem for UploadFs {
|
||||
fn open<'a>(
|
||||
&'a self,
|
||||
path: &'a webdav_handler::davpath::DavPath,
|
||||
options: webdav_handler::fs::OpenOptions,
|
||||
) -> webdav_handler::fs::FsFuture<Box<dyn webdav_handler::fs::DavFile>> {
|
||||
todo!()
|
||||
}
|
||||
|
||||
fn read_dir<'a>(
|
||||
&'a self,
|
||||
path: &'a webdav_handler::davpath::DavPath,
|
||||
meta: webdav_handler::fs::ReadDirMeta,
|
||||
) -> webdav_handler::fs::FsFuture<
|
||||
webdav_handler::fs::FsStream<Box<dyn webdav_handler::fs::DavDirEntry>>,
|
||||
> {
|
||||
todo!()
|
||||
}
|
||||
|
||||
fn metadata<'a>(
|
||||
&'a self,
|
||||
path: &'a webdav_handler::davpath::DavPath,
|
||||
) -> webdav_handler::fs::FsFuture<Box<dyn webdav_handler::fs::DavMetaData>> {
|
||||
todo!()
|
||||
}
|
||||
|
||||
fn symlink_metadata<'a>(
|
||||
&'a self,
|
||||
path: &'a webdav_handler::davpath::DavPath,
|
||||
) -> webdav_handler::fs::FsFuture<Box<dyn webdav_handler::fs::DavMetaData>> {
|
||||
self.metadata(path)
|
||||
}
|
||||
|
||||
fn create_dir<'a>(
|
||||
&'a self,
|
||||
path: &'a webdav_handler::davpath::DavPath,
|
||||
) -> webdav_handler::fs::FsFuture<()> {
|
||||
todo!()
|
||||
}
|
||||
|
||||
fn remove_dir<'a>(
|
||||
&'a self,
|
||||
path: &'a webdav_handler::davpath::DavPath,
|
||||
) -> webdav_handler::fs::FsFuture<()> {
|
||||
todo!()
|
||||
}
|
||||
|
||||
fn remove_file<'a>(
|
||||
&'a self,
|
||||
path: &'a webdav_handler::davpath::DavPath,
|
||||
) -> webdav_handler::fs::FsFuture<()> {
|
||||
todo!()
|
||||
}
|
||||
|
||||
fn rename<'a>(
|
||||
&'a self,
|
||||
from: &'a webdav_handler::davpath::DavPath,
|
||||
to: &'a webdav_handler::davpath::DavPath,
|
||||
) -> webdav_handler::fs::FsFuture<()> {
|
||||
todo!()
|
||||
}
|
||||
|
||||
fn copy<'a>(
|
||||
&'a self,
|
||||
from: &'a webdav_handler::davpath::DavPath,
|
||||
to: &'a webdav_handler::davpath::DavPath,
|
||||
) -> webdav_handler::fs::FsFuture<()> {
|
||||
todo!()
|
||||
}
|
||||
|
||||
fn have_props<'a>(
|
||||
&'a self,
|
||||
path: &'a webdav_handler::davpath::DavPath,
|
||||
) -> std::pin::Pin<Box<dyn futures::prelude::Future<Output = bool> + Send + 'a>> {
|
||||
Box::pin(futures::prelude::future::ready(false))
|
||||
}
|
||||
|
||||
fn patch_props<'a>(
|
||||
&'a self,
|
||||
path: &'a webdav_handler::davpath::DavPath,
|
||||
patch: Vec<(bool, webdav_handler::fs::DavProp)>,
|
||||
) -> webdav_handler::fs::FsFuture<Vec<(axum::http::StatusCode, webdav_handler::fs::DavProp)>>
|
||||
{
|
||||
todo!()
|
||||
}
|
||||
|
||||
fn get_props<'a>(
|
||||
&'a self,
|
||||
path: &'a webdav_handler::davpath::DavPath,
|
||||
do_content: bool,
|
||||
) -> webdav_handler::fs::FsFuture<Vec<webdav_handler::fs::DavProp>> {
|
||||
todo!()
|
||||
}
|
||||
|
||||
fn get_prop<'a>(
|
||||
&'a self,
|
||||
path: &'a webdav_handler::davpath::DavPath,
|
||||
prop: webdav_handler::fs::DavProp,
|
||||
) -> webdav_handler::fs::FsFuture<xmltree::Element> {
|
||||
todo!()
|
||||
}
|
||||
|
||||
fn get_quota(&self) -> webdav_handler::fs::FsFuture<(u64, Option<u64>)> {
|
||||
todo!()
|
||||
}
|
||||
}
|
BIN
static/ferris.png
Normal file
BIN
static/ferris.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 61 KiB |
Loading…
Reference in a new issue