mirror of https://github.com/veypi/OneAuth.git
update
parent
ad4f9d4b6a
commit
664bf9c922
@ -0,0 +1,92 @@
|
|||||||
|
//
|
||||||
|
// upload.rs
|
||||||
|
// Copyright (C) 2023 veypi <i@veypi.com>
|
||||||
|
// 2023-10-03 21:50
|
||||||
|
// Distributed under terms of the MIT license.
|
||||||
|
//
|
||||||
|
//
|
||||||
|
|
||||||
|
use actix_multipart::form::{tempfile::TempFile, MultipartForm};
|
||||||
|
use actix_web::{post, web, Responder};
|
||||||
|
use proc::access_read;
|
||||||
|
use tracing::{info, warn};
|
||||||
|
|
||||||
|
use crate::{AppState, Error, Result};
|
||||||
|
|
||||||
|
#[derive(Debug, MultipartForm)]
|
||||||
|
struct UploadForm {
|
||||||
|
files: Vec<TempFile>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[post("/upload/")]
|
||||||
|
#[access_read("app")]
|
||||||
|
async fn save_files(
|
||||||
|
MultipartForm(form): MultipartForm<UploadForm>,
|
||||||
|
stat: web::Data<AppState>,
|
||||||
|
) -> Result<impl Responder> {
|
||||||
|
let l = form.files.len();
|
||||||
|
let mut res: Vec<String> = Vec::new();
|
||||||
|
info!("!|||||||||||_{}_|", l);
|
||||||
|
for f in form.files {
|
||||||
|
info!("saving to {:#?}", f);
|
||||||
|
let fname = f.file_name.unwrap();
|
||||||
|
let path = format!("{}tmp/{}", stat.media_path, fname);
|
||||||
|
info!("saving to {path}");
|
||||||
|
match f.file.persist(path) {
|
||||||
|
Ok(t) => {
|
||||||
|
info!("{:#?}", t);
|
||||||
|
res.push(format!("/media/tmp/{}", fname))
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
warn!("{}", e);
|
||||||
|
return Err(Error::InternalServerError);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(web::Json(res))
|
||||||
|
}
|
||||||
|
|
||||||
|
// #[actix_web::main]
|
||||||
|
// async fn main() -> std::io::Result<()> {
|
||||||
|
// HttpServer::new(|| {
|
||||||
|
// App::new()
|
||||||
|
// .wrap(middleware::Logger::default())
|
||||||
|
// .app_data(TempFileConfig::default().directory("./tmp"))
|
||||||
|
// .service(
|
||||||
|
// web::resource("/")
|
||||||
|
// .route(web::get().to(index))
|
||||||
|
// .route(web::post().to(save_files)),
|
||||||
|
// )
|
||||||
|
// })
|
||||||
|
// .bind(("127.0.0.1", 8080))?
|
||||||
|
// .workers(2)
|
||||||
|
// .run()
|
||||||
|
// .await
|
||||||
|
// }
|
||||||
|
|
||||||
|
// /// Example of the old manual way of processing multipart forms.
|
||||||
|
// #[allow(unused)]
|
||||||
|
// async fn save_file_manual(mut payload: Multipart) -> Result<HttpResponse, Error> {
|
||||||
|
// // iterate over multipart stream
|
||||||
|
// while let Some(mut field) = payload.try_next().await? {
|
||||||
|
// // A multipart/form-data stream has to contain `content_disposition`
|
||||||
|
// let content_disposition = field.content_disposition();
|
||||||
|
|
||||||
|
// let filename = content_disposition
|
||||||
|
// .get_filename()
|
||||||
|
// .map_or_else(|| Uuid::new_v4().to_string(), sanitize_filename::sanitize);
|
||||||
|
// let filepath = format!("./tmp/{filename}");
|
||||||
|
|
||||||
|
// // File::create is blocking operation, use threadpool
|
||||||
|
// let mut f = web::block(|| std::fs::File::create(filepath)).await??;
|
||||||
|
|
||||||
|
// // Field in turn is stream of *Bytes* object
|
||||||
|
// while let Some(chunk) = field.try_next().await? {
|
||||||
|
// // filesystem operations are blocking, we have to use threadpool
|
||||||
|
// f = web::block(move || f.write_all(&chunk).map(|_| f)).await??;
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// Ok(HttpResponse::Ok().into())
|
||||||
|
// }
|
@ -0,0 +1,121 @@
|
|||||||
|
//
|
||||||
|
// fs.rs
|
||||||
|
// Copyright (C) 2023 veypi <i@veypi.com>
|
||||||
|
// 2023-10-02 22:51
|
||||||
|
// Distributed under terms of the MIT license.
|
||||||
|
//
|
||||||
|
//
|
||||||
|
|
||||||
|
use std::{fs, path::Path};
|
||||||
|
|
||||||
|
use actix_web::web;
|
||||||
|
|
||||||
|
use dav_server::{
|
||||||
|
actix::{DavRequest, DavResponse},
|
||||||
|
body::Body,
|
||||||
|
fakels::FakeLs,
|
||||||
|
localfs::LocalFs,
|
||||||
|
DavConfig, DavHandler,
|
||||||
|
};
|
||||||
|
|
||||||
|
use http::Response;
|
||||||
|
use http_auth_basic::Credentials;
|
||||||
|
use sea_orm::{ColumnTrait, EntityTrait, QueryFilter};
|
||||||
|
use tracing::{info, warn};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
models::{self, UserPlugin},
|
||||||
|
AppState, Error, Result,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn core() -> DavHandler {
|
||||||
|
DavHandler::builder()
|
||||||
|
.locksystem(FakeLs::new())
|
||||||
|
.strip_prefix("/file/")
|
||||||
|
.build_handler()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn dav_handler(
|
||||||
|
req: DavRequest,
|
||||||
|
davhandler: web::Data<DavHandler>,
|
||||||
|
stat: web::Data<AppState>,
|
||||||
|
) -> DavResponse {
|
||||||
|
let root = stat.fs_root.clone();
|
||||||
|
match handle_file(req, stat).await {
|
||||||
|
Ok((p, req)) => {
|
||||||
|
let p = Path::new(&root).join(p);
|
||||||
|
if !p.exists() {
|
||||||
|
match fs::create_dir_all(p.clone()) {
|
||||||
|
Ok(_) => {}
|
||||||
|
Err(e) => {
|
||||||
|
warn!("{}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
info!("mount {}", p.to_str().unwrap());
|
||||||
|
let config = DavConfig::new().filesystem(LocalFs::new(p, false, false, true));
|
||||||
|
davhandler.handle_with(config, req.request).await.into()
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
warn!("handle file failed: {}", e);
|
||||||
|
Response::builder()
|
||||||
|
.status(401)
|
||||||
|
.header("WWW-Authenticate", "Basic realm=\"file\"")
|
||||||
|
.body(Body::from("please auth".to_string()))
|
||||||
|
.unwrap()
|
||||||
|
.into()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn handle_file(req: DavRequest, stat: web::Data<AppState>) -> Result<(String, DavRequest)> {
|
||||||
|
let p = req.request.uri();
|
||||||
|
let headers = req.request.headers();
|
||||||
|
let m = req.request.method();
|
||||||
|
// handle_authorization(req.request.headers());
|
||||||
|
info!("access {} to {}", m, p);
|
||||||
|
let auth_token = headers.get("auth_token");
|
||||||
|
let authorization = headers.get("authorization");
|
||||||
|
let app_id = match headers.get("app_id") {
|
||||||
|
Some(i) => i.to_str().unwrap_or(""),
|
||||||
|
None => "",
|
||||||
|
};
|
||||||
|
match auth_token {
|
||||||
|
Some(t) => match models::Token::from(t.to_str().unwrap_or("")) {
|
||||||
|
Ok(t) => {
|
||||||
|
if t.is_valid() {
|
||||||
|
if app_id != "" {
|
||||||
|
// 只有秘钥才能访问app数据
|
||||||
|
if t.can_read("app", app_id) {
|
||||||
|
return Ok((format!("app/{}/", app_id), req));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return Ok((format!("user/{}/", t.id), req));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(_) => {}
|
||||||
|
},
|
||||||
|
None => {}
|
||||||
|
}
|
||||||
|
match authorization {
|
||||||
|
Some(au) => {
|
||||||
|
let credentials =
|
||||||
|
Credentials::from_header(au.to_str().unwrap_or("").to_string()).unwrap();
|
||||||
|
info!("{}|{}", credentials.user_id, credentials.password);
|
||||||
|
match models::user::Entity::find()
|
||||||
|
.filter(models::user::Column::Username.eq(credentials.user_id))
|
||||||
|
.one(stat.db())
|
||||||
|
.await?
|
||||||
|
{
|
||||||
|
Some(u) => {
|
||||||
|
u.check_pass(&credentials.password)?;
|
||||||
|
return Ok((format!("user/{}/", u.id), req));
|
||||||
|
}
|
||||||
|
None => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => {}
|
||||||
|
}
|
||||||
|
Err(Error::NotAuthed)
|
||||||
|
}
|
@ -1,34 +1,22 @@
|
|||||||
<template>
|
<template>
|
||||||
<q-item
|
<q-item v-ripple clickable tag="a" :href="link" :to="to">
|
||||||
clickable
|
<q-item-section v-if="icon" avatar>
|
||||||
tag="a"
|
|
||||||
target="_blank"
|
|
||||||
:href="link"
|
|
||||||
>
|
|
||||||
<q-item-section
|
|
||||||
v-if="icon"
|
|
||||||
avatar
|
|
||||||
>
|
|
||||||
<q-icon :name="icon" />
|
<q-icon :name="icon" />
|
||||||
</q-item-section>
|
</q-item-section>
|
||||||
|
|
||||||
<q-item-section>
|
<q-item-section>
|
||||||
<q-item-label>{{ title }}</q-item-label>
|
<q-item-label>{{ title }}</q-item-label>
|
||||||
<q-item-label caption>{{ caption }}</q-item-label>
|
<!-- <q-item-label caption>{{ caption }}</q-item-label> -->
|
||||||
</q-item-section>
|
</q-item-section>
|
||||||
</q-item>
|
</q-item>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
export interface EssentialLinkProps {
|
|
||||||
title: string;
|
import { MenuLink } from 'src/models'
|
||||||
caption?: string;
|
|
||||||
link?: string;
|
withDefaults(defineProps<MenuLink>(), {
|
||||||
icon?: string;
|
|
||||||
}
|
|
||||||
withDefaults(defineProps<EssentialLinkProps>(), {
|
|
||||||
caption: '',
|
caption: '',
|
||||||
link: '#',
|
|
||||||
icon: '',
|
icon: '',
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
@ -0,0 +1,30 @@
|
|||||||
|
<!--
|
||||||
|
* main.vue
|
||||||
|
* Copyright (C) 2023 veypi <i@veypi.com>
|
||||||
|
* 2023-10-04 10:56
|
||||||
|
* Distributed under terms of the MIT license.
|
||||||
|
-->
|
||||||
|
<template>
|
||||||
|
<q-list>
|
||||||
|
<!-- <q-item-label header> -->
|
||||||
|
<!-- Essential Links -->
|
||||||
|
<!-- </q-item-label> -->
|
||||||
|
|
||||||
|
<EssentialLink v-for="link in menu.list" :key="link.title" v-bind="link" />
|
||||||
|
</q-list>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import EssentialLink from 'src/components/EssentialLink.vue';
|
||||||
|
import { useMenuStore } from 'src/stores/menu';
|
||||||
|
import { onMounted } from 'vue';
|
||||||
|
let menu = useMenuStore()
|
||||||
|
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
console.log('loading main menu')
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped></style>
|
||||||
|
|
@ -1,56 +1,87 @@
|
|||||||
<template>
|
<template>
|
||||||
<div @click="click">
|
<div @click="click">
|
||||||
<input ref="file" type="file" hidden @change="upload">
|
<input enctype="multipart/form-data" ref="file" name="files" multiple type="file" hidden @change="upload">
|
||||||
<slot></slot>
|
<slot></slot>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { createClient } from "webdav";
|
// import { createClient } from "webdav";
|
||||||
import { ref } from 'vue';
|
import { ref } from 'vue';
|
||||||
import { useAppStore } from "src/stores/app";
|
import axios from "axios";
|
||||||
|
|
||||||
let file = ref<HTMLInputElement>()
|
let file = ref<HTMLInputElement>()
|
||||||
let app = useAppStore();
|
|
||||||
let emits = defineEmits<{
|
let emits = defineEmits<{
|
||||||
(e: 'success', v: string): void
|
(e: 'success', v: string): void
|
||||||
(e: 'failed'): void
|
(e: 'failed'): void
|
||||||
}>()
|
}>()
|
||||||
let props = withDefaults(defineProps<{
|
let props = withDefaults(defineProps<{
|
||||||
url: string
|
multiple?: boolean,
|
||||||
}>(), {
|
}>(), {
|
||||||
url: '',
|
multiple: false
|
||||||
})
|
})
|
||||||
|
|
||||||
function click() {
|
function click() {
|
||||||
file.value?.dispatchEvent(new MouseEvent('click'))
|
file.value?.dispatchEvent(new MouseEvent('click'))
|
||||||
}
|
}
|
||||||
|
|
||||||
let prefix = '/file/public/app/' + app.id + '/'
|
const upload = (evt: Event) => {
|
||||||
|
evt.preventDefault()
|
||||||
let client = createClient(prefix,
|
let f = (evt.target as HTMLInputElement).files as any
|
||||||
{ headers: { auth_token: localStorage.getItem('auth_token') || '' } })
|
var data = new FormData();
|
||||||
|
console.log(f)
|
||||||
async function upload() {
|
for (let i of f) {
|
||||||
let list = file.value?.files || []
|
console.log(i)
|
||||||
if (list.length) {
|
data.append('files', i, i.data)
|
||||||
let reader = new FileReader()
|
|
||||||
reader.onload = function (event) {
|
|
||||||
var res = event.target?.result
|
|
||||||
// let data = new Blob([res])
|
|
||||||
let url = props.url.replaceAll('.', '.' + new Date().getTime().toString() + '.')
|
|
||||||
client.putFileContents(url, res).then(e => {
|
|
||||||
if (e) {
|
|
||||||
emits('success', prefix + url)
|
|
||||||
} else {
|
|
||||||
emits('failed')
|
|
||||||
}
|
}
|
||||||
})
|
axios.post("/api/upload/", data, {
|
||||||
}
|
headers: {
|
||||||
reader.readAsArrayBuffer(list[0])
|
"Content-Type": 'multipart/form-data',
|
||||||
} else {
|
'auth_token': localStorage.getItem('auth_token')
|
||||||
}
|
}
|
||||||
|
}).then(e => {
|
||||||
|
console.log(e.data)
|
||||||
|
emits('success', props.multiple ? e.data : e.data[0])
|
||||||
|
})
|
||||||
|
// var token = sessionStorage.getItem('token')
|
||||||
|
// const config = {
|
||||||
|
// headers: {
|
||||||
|
// 'Content-Type': 'multipart/form-data'
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// window.API.post('https://110.10.56.10:8000/images/?token=' + token, data, config)
|
||||||
|
// .then(response => this.$router.push('/listImage'))
|
||||||
|
// .catch((error) => {
|
||||||
|
// console.log(JSON.stringify(error))
|
||||||
|
// })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// async function dav_upload() {
|
||||||
|
// let prefix = '/file/public/app/' + app.id + '/'
|
||||||
|
// let client = createClient(prefix,
|
||||||
|
// { headers: { auth_token: localStorage.getItem('auth_token') || '' } })
|
||||||
|
// let list = file.value?.files || []
|
||||||
|
// if (list.length) {
|
||||||
|
// let reader = new FileReader()
|
||||||
|
// reader.onload = function (event) {
|
||||||
|
// var res = event.target?.result
|
||||||
|
// // let data = new Blob([res])
|
||||||
|
// let url = props.url.replaceAll('.', '.' + new Date().getTime().toString() + '.')
|
||||||
|
// client.putFileContents(url, res).then(e => {
|
||||||
|
// if (e) {
|
||||||
|
// emits('success', prefix + url)
|
||||||
|
// } else {
|
||||||
|
// emits('failed')
|
||||||
|
// }
|
||||||
|
// })
|
||||||
|
// }
|
||||||
|
// reader.readAsArrayBuffer(list[0])
|
||||||
|
// } else {
|
||||||
|
// }
|
||||||
|
// }
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped></style>
|
<style scoped></style>
|
||||||
|
@ -0,0 +1,53 @@
|
|||||||
|
<!--
|
||||||
|
* AppLayout.vue
|
||||||
|
* Copyright (C) 2023 veypi <i@veypi.com>
|
||||||
|
* 2023-10-04 10:46
|
||||||
|
* Distributed under terms of the MIT license.
|
||||||
|
-->
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<h1>{{ app.name }}</h1>
|
||||||
|
<router-view :data="{ a: 1 }" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import msg from '@veypi/msg';
|
||||||
|
import api from 'src/boot/api';
|
||||||
|
import { modelsApp } from 'src/models';
|
||||||
|
import { useMenuStore } from 'src/stores/menu';
|
||||||
|
import { computed, watch, ref, onMounted, provide, onBeforeUnmount } from 'vue';
|
||||||
|
import { useRoute } from 'vue-router';
|
||||||
|
let route = useRoute();
|
||||||
|
let menu = useMenuStore()
|
||||||
|
|
||||||
|
let id = computed(() => route.params.id)
|
||||||
|
let app = ref({} as modelsApp)
|
||||||
|
|
||||||
|
provide('app', app)
|
||||||
|
|
||||||
|
const sync_app = () => {
|
||||||
|
api.app.get(id.value as string).then((e: modelsApp) => {
|
||||||
|
app.value = e
|
||||||
|
}).catch(e => {
|
||||||
|
msg.Warn('sync app data failed: ' + e)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
watch(id, () => {
|
||||||
|
sync_app()
|
||||||
|
})
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
sync_app()
|
||||||
|
menu.set([
|
||||||
|
|
||||||
|
])
|
||||||
|
})
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
menu.load_default()
|
||||||
|
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped></style>
|
||||||
|
|
@ -0,0 +1,28 @@
|
|||||||
|
{
|
||||||
|
"name": "@veypi/oaer",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "",
|
||||||
|
"main": "src/index.ts",
|
||||||
|
"scripts": {
|
||||||
|
"test": "echo \"Error: no test specified\" && exit 1"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"mitt": "^3.0.0",
|
||||||
|
"vue": "^3.2.20"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@veypi/one-icon": "2.0.6",
|
||||||
|
"@vitejs/plugin-vue": "^1.9.3",
|
||||||
|
"autoprefixer": "^9.8.8",
|
||||||
|
"axios": "^0.24.0",
|
||||||
|
"js-base64": "^3.7.2",
|
||||||
|
"naive-ui": "^2.19.11",
|
||||||
|
"postcss": "^7.0.39",
|
||||||
|
"tailwindcss": "npm:@tailwindcss/postcss7-compat@2.1.0",
|
||||||
|
"typescript": "^4.4.3",
|
||||||
|
"vite": "^2.6.4",
|
||||||
|
"vue-tsc": "^0.3.0"
|
||||||
|
},
|
||||||
|
"author": "veypi",
|
||||||
|
"license": "MIT"
|
||||||
|
}
|
@ -0,0 +1,6 @@
|
|||||||
|
module.exports = {
|
||||||
|
plugins: {
|
||||||
|
tailwindcss: {},
|
||||||
|
autoprefixer: {},
|
||||||
|
},
|
||||||
|
}
|
@ -0,0 +1,86 @@
|
|||||||
|
import axios from 'axios'
|
||||||
|
import evt from '../evt'
|
||||||
|
import {Cfg} from './setting'
|
||||||
|
|
||||||
|
|
||||||
|
function getQueryVariable(variable: string) {
|
||||||
|
let query = window.location.search.substring(1)
|
||||||
|
let vars = query.split('&')
|
||||||
|
for (let i = 0; i < vars.length; i++) {
|
||||||
|
let pair = vars[i].split('=')
|
||||||
|
if (pair[0] == variable) {
|
||||||
|
return pair[1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
|
function baseRequests(url: string, method: any = 'GET', query: any, data: any, success: any, fail?: Function, header?: any) {
|
||||||
|
let headers = {
|
||||||
|
auth_token: Cfg.token.value || decodeURIComponent(getQueryVariable('token') as string),
|
||||||
|
uuid: Cfg.uuid.value,
|
||||||
|
}
|
||||||
|
if (header) {
|
||||||
|
headers = Object.assign(headers, header)
|
||||||
|
}
|
||||||
|
return axios({
|
||||||
|
url: url,
|
||||||
|
params: query,
|
||||||
|
data: data,
|
||||||
|
method: method,
|
||||||
|
headers: headers,
|
||||||
|
}).then((res: any) => {
|
||||||
|
if ('auth_token' in res.headers) {
|
||||||
|
Cfg.token.value = res.headers.auth_token
|
||||||
|
}
|
||||||
|
if ('redirect_url' in res.headers) {
|
||||||
|
window.location.href = res.headers.redirect_url
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (method === 'HEAD') {
|
||||||
|
success(res.headers)
|
||||||
|
} else {
|
||||||
|
success(res.data)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((e: any) => {
|
||||||
|
if (e.response && e.response.status === 401) {
|
||||||
|
evt.emit('logout')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
console.log(e)
|
||||||
|
if (e.response && e.response.status === 500) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (typeof fail === 'function') {
|
||||||
|
fail(e.response)
|
||||||
|
} else if (e.response && e.response.status === 400) {
|
||||||
|
console.log(400)
|
||||||
|
} else {
|
||||||
|
console.log(e.request)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const ajax = {
|
||||||
|
get(url: '', data = {}, success = {}, fail?: Function, header?: any) {
|
||||||
|
return baseRequests(url, 'GET', data, {}, success, fail, header)
|
||||||
|
},
|
||||||
|
head(url: '', data = {}, success = {}, fail?: Function, header?: any) {
|
||||||
|
return baseRequests(url, 'HEAD', data, {}, success, fail, header)
|
||||||
|
},
|
||||||
|
delete(url: '', data = {}, success = {}, fail?: Function, header?: any) {
|
||||||
|
return baseRequests(url, 'DELETE', data, {}, success, fail, header)
|
||||||
|
},
|
||||||
|
post(url: '', data = {}, success = {}, fail?: Function, header?: any) {
|
||||||
|
return baseRequests(url, 'POST', {}, data, success, fail, header)
|
||||||
|
},
|
||||||
|
put(url: '', data = {}, success = {}, fail?: Function, header?: any) {
|
||||||
|
return baseRequests(url, 'PUT', {}, data, success, fail, header)
|
||||||
|
},
|
||||||
|
patch(url: '', data = {}, success = {}, fail?: Function, header?: any) {
|
||||||
|
return baseRequests(url, 'PATCH', {}, data, success, fail, header)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ajax
|
@ -0,0 +1,18 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2019 light <veypi@light-laptop>
|
||||||
|
*
|
||||||
|
* Distributed under terms of the MIT license.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import user from './user'
|
||||||
|
import app from './app'
|
||||||
|
import {Cfg} from './setting'
|
||||||
|
|
||||||
|
|
||||||
|
const api = {
|
||||||
|
user: user,
|
||||||
|
app: app,
|
||||||
|
}
|
||||||
|
|
||||||
|
export {api, Cfg}
|
||||||
|
export default api
|
@ -0,0 +1,65 @@
|
|||||||
|
import evt from '../evt'
|
||||||
|
|
||||||
|
export type SuccessFunction<T> = (e: any) => void;
|
||||||
|
export type FailedFunction<T> = (e: any) => void;
|
||||||
|
|
||||||
|
const Code = {
|
||||||
|
42011: '无操作权限',
|
||||||
|
22031: '资源不存在 或 您无权操作该资源',
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Interface {
|
||||||
|
private readonly method: Function
|
||||||
|
private readonly api: string
|
||||||
|
private readonly data: any
|
||||||
|
private readonly header: any
|
||||||
|
|
||||||
|
constructor(method: Function, api: string, data?: any, headers?: any) {
|
||||||
|
this.method = method
|
||||||
|
this.api = api
|
||||||
|
this.data = data
|
||||||
|
this.header = headers
|
||||||
|
}
|
||||||
|
|
||||||
|
Start(success?: SuccessFunction<any>, fail?: FailedFunction<any>) {
|
||||||
|
const newFail = function (data: any) {
|
||||||
|
if (data) {
|
||||||
|
if (data.code === 40001) {
|
||||||
|
// no login
|
||||||
|
evt.emit('logout')
|
||||||
|
return
|
||||||
|
// @ts-ignore
|
||||||
|
} else if (data.code === 42011 && window.$msg) {
|
||||||
|
// @ts-ignore
|
||||||
|
window.$msg.warning('无权限')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-ignore
|
||||||
|
// @ts-ignore
|
||||||
|
if (data && data.code && Code[data.code]) {
|
||||||
|
}
|
||||||
|
if (fail) {
|
||||||
|
fail(data.err)
|
||||||
|
// @ts-ignore
|
||||||
|
} else if (window.$msg) {
|
||||||
|
// @ts-ignore
|
||||||
|
window.$msg.warning(data.err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const newSuccess = function (data: any) {
|
||||||
|
if (Number(data.status) === 1) {
|
||||||
|
if (success) {
|
||||||
|
success(data.content)
|
||||||
|
// @ts-ignore
|
||||||
|
} else if (window.$msg) {
|
||||||
|
// @ts-ignore
|
||||||
|
window.$msg.warning('ok')
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
newFail(data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.method(this.api, this.data, newSuccess, newFail, this.header)
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,33 @@
|
|||||||
|
import {Base64} from 'js-base64'
|
||||||
|
import {Interface} from './interface'
|
||||||
|
import ajax from './ajax'
|
||||||
|
import {Cfg} from './setting'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
local: () => Cfg.BaseUrl() + 'user/',
|
||||||
|
register(username: string, password: string, prop?: any) {
|
||||||
|
const data = Object.assign({
|
||||||
|
username: username,
|
||||||
|
password: Base64.encode(password),
|
||||||
|
}, prop)
|
||||||
|
return new Interface(ajax.post, this.local(), data)
|
||||||
|
},
|
||||||
|
login(username: string, password: string) {
|
||||||
|
return new Interface(ajax.head, this.local() + username, {
|
||||||
|
UidType: 'username',
|
||||||
|
password: Base64.encode(password),
|
||||||
|
})
|
||||||
|
},
|
||||||
|
search(q: string) {
|
||||||
|
return new Interface(ajax.get, this.local(), {username: q})
|
||||||
|
},
|
||||||
|
get(id: number) {
|
||||||
|
return new Interface(ajax.get, this.local() + id)
|
||||||
|
},
|
||||||
|
list() {
|
||||||
|
return new Interface(ajax.get, this.local())
|
||||||
|
},
|
||||||
|
update(id: number, props: any) {
|
||||||
|
return new Interface(ajax.patch, this.local() + id, props)
|
||||||
|
},
|
||||||
|
}
|
@ -0,0 +1 @@
|
|||||||
|
!function(t){var e,n,o,i,c,d='<svg><symbol id="icon-close" viewBox="0 0 1024 1024"><path d="M176.661601 817.172881C168.472798 825.644055 168.701706 839.149636 177.172881 847.338438 185.644056 855.527241 199.149636 855.298332 207.338438 846.827157L826.005105 206.827157C834.193907 198.355983 833.964998 184.850403 825.493824 176.661601 817.02265 168.472798 803.517069 168.701706 795.328267 177.172881L176.661601 817.172881Z" ></path><path d="M795.328267 846.827157C803.517069 855.298332 817.02265 855.527241 825.493824 847.338438 833.964998 839.149636 834.193907 825.644055 826.005105 817.172881L207.338438 177.172881C199.149636 168.701706 185.644056 168.472798 177.172881 176.661601 168.701706 184.850403 168.472798 198.355983 176.661601 206.827157L795.328267 846.827157Z" ></path></symbol><symbol id="icon-logout" viewBox="0 0 1024 1024"><path d="M856.8 389.8c-18.9-44.7-45.9-84.8-80.4-119.2-18.9-18.9-39.5-35.6-61.7-49.9-10-6.5-23.3 0.6-23.3 12.6 0 5.1 2.6 9.9 6.9 12.6 95 61.5 158 168.5 158 289.8 0 190.3-154.8 345-345 345s-345-154.8-345-345c0-122.4 64.1-230.2 160.5-291.4 4.4-2.8 7-7.6 7-12.7 0-11.8-13.1-19.1-23.1-12.8-23.2 14.7-44.8 32-64.6 51.8-34.4 34.4-61.5 74.5-80.4 119.2-19.6 46.2-29.5 95.3-29.5 146s9.9 99.7 29.5 146c18.9 44.7 45.9 84.8 80.4 119.2 34.4 34.4 74.5 61.5 119.2 80.4 46.2 19.6 95.3 29.5 146 29.5 50.6 0 99.7-9.9 146-29.5 44.7-18.9 84.8-45.9 119.2-80.4s61.5-74.5 80.4-119.2c19.6-46.2 29.5-95.3 29.5-146s-10-99.8-29.6-146z" fill="" ></path><path d="M512 431.1c-8.8 0-16-7.2-16-16V98.2c0-8.8 7.2-16 16-16s16 7.2 16 16V415c0 8.9-7.2 16.1-16 16.1z" fill="" ></path></symbol></svg>',s=(s=document.getElementsByTagName("script"))[s.length-1].getAttribute("data-injectcss"),l=function(t,e){e.parentNode.insertBefore(t,e)};if(s&&!t.__iconfont__svg__cssinject__){t.__iconfont__svg__cssinject__=!0;try{document.write("<style>.svgfont {display: inline-block;width: 1em;height: 1em;fill: currentColor;vertical-align: -0.1em;font-size:16px;}</style>")}catch(t){console&&console.log(t)}}function a(){c||(c=!0,o())}function r(){try{i.documentElement.doScroll("left")}catch(t){return void setTimeout(r,50)}a()}e=function(){var t,e;(e=document.createElement("div")).innerHTML=d,d=null,(t=e.getElementsByTagName("svg")[0])&&(t.setAttribute("aria-hidden","true"),t.style.position="absolute",t.style.width=0,t.style.height=0,t.style.overflow="hidden",e=t,(t=document.body).firstChild?l(e,t.firstChild):t.appendChild(e))},document.addEventListener?~["complete","loaded","interactive"].indexOf(document.readyState)?setTimeout(e,0):(n=function(){document.removeEventListener("DOMContentLoaded",n,!1),e()},document.addEventListener("DOMContentLoaded",n,!1)):document.attachEvent&&(o=e,i=t.document,c=!1,r(),i.onreadystatechange=function(){"complete"==i.readyState&&(i.onreadystatechange=null,a())})}(window);
|
@ -0,0 +1,36 @@
|
|||||||
|
<template>
|
||||||
|
<div class="w-full px-3">
|
||||||
|
<div class="h-16 flex justify-between items-center">
|
||||||
|
<span style="color: #777">我的应用</span>
|
||||||
|
<span @click="Cfg.host.value?goto(Cfg.host.value):redirect('/')" class="cursor-pointer"
|
||||||
|
style="color:#f36828">应用中心</span>
|
||||||
|
</div>
|
||||||
|
<div class="grid grid-cols-5">
|
||||||
|
<template v-for="(ap,ai) of apps"
|
||||||
|
:key="ai">
|
||||||
|
<div class="mx-2" @click="redirect(ap.Host)" v-if="ap.UUID !== Cfg.uuid.value">
|
||||||
|
<n-avatar v-if="ap.Icon" size="2" :src="Cfg.host.value+ ap.Icon" round></n-avatar>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
<hr class="mt-10" style="border:none;border-top:1px solid #777;">
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import {modelsApp} from '../models'
|
||||||
|
import {Cfg} from '../api'
|
||||||
|
|
||||||
|
let props = withDefaults(defineProps<{
|
||||||
|
apps: modelsApp[]
|
||||||
|
}>(), {})
|
||||||
|
|
||||||
|
function goto(url: string) {
|
||||||
|
window.open(url, '_blank')
|
||||||
|
}
|
||||||
|
|
||||||
|
function redirect(url: string) {
|
||||||
|
window.location.href = url
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<style scoped>
|
||||||
|
</style>
|
@ -0,0 +1,47 @@
|
|||||||
|
<template>
|
||||||
|
<div class="w-full px-3">
|
||||||
|
<div class="h-16 flex justify-between items-center">
|
||||||
|
<span style="color: #777">
|
||||||
|
我的云盘
|
||||||
|
</span>
|
||||||
|
<span class="cursor-pointer" style="color:#f36828">文件中心</span>
|
||||||
|
</div>
|
||||||
|
<div class="">
|
||||||
|
{{ usr.Used }} KB / {{ usr.Space }} GB
|
||||||
|
<n-progress type="line" color="#0f0" rail-color="#fff" :percentage="1" indicator-text-color="#f00" />
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-center">
|
||||||
|
<n-button @click="showModal = true" type="primary">获取挂载链接</n-button>
|
||||||
|
</div>
|
||||||
|
<n-modal v-model:show="showModal">
|
||||||
|
<n-card style="width: 600px;" title="云盘挂载地址" :bordered="false" size="huge">
|
||||||
|
<template #header-extra>复制</template>
|
||||||
|
{{ Cfg.userFileUrl() }}
|
||||||
|
<template #footer> 挂载说明</template>
|
||||||
|
</n-card>
|
||||||
|
</n-modal>
|
||||||
|
<hr class="mt-10" style="border:none;border-top:1px solid #777;">
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { createClient } from 'webdav'
|
||||||
|
import { modelsUser } from '../models'
|
||||||
|
import { onMounted, ref } from 'vue'
|
||||||
|
import { Cfg } from '../api'
|
||||||
|
|
||||||
|
let showModal = ref(false)
|
||||||
|
let props = withDefaults(defineProps<{
|
||||||
|
usr: modelsUser
|
||||||
|
}>(), {})
|
||||||
|
|
||||||
|
let client = createClient('http://127.0.0.1:4001/file/usr/',
|
||||||
|
{ headers: { auth_token: localStorage.getItem('auth_token') as string } })
|
||||||
|
onMounted(() => {
|
||||||
|
client.stat('').then((e) => {
|
||||||
|
console.log(e)
|
||||||
|
}).catch((e) => {
|
||||||
|
console.log(e)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
<style scoped></style>
|
@ -0,0 +1,7 @@
|
|||||||
|
<template>
|
||||||
|
<div></div>
|
||||||
|
</template>
|
||||||
|
<script lang="js" setup>
|
||||||
|
</script>
|
||||||
|
<style scoped>
|
||||||
|
</style>
|
@ -0,0 +1,91 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<div @click="setValue(true)">
|
||||||
|
<slot>
|
||||||
|
</slot>
|
||||||
|
</div>
|
||||||
|
<div @click.self="setValue(false)" class="core" style="height: 100vh;width: 100vw;" v-if="props.modelValue">
|
||||||
|
<div style="height: 100%; width: 300px" class="core-right">
|
||||||
|
<transition appear enter-active-class="animate__slideInRight">
|
||||||
|
<div class="flex right-title animate__animated animate__faster px-3">
|
||||||
|
<div class="flex-grow text-left" style="font-size: 1.2rem">
|
||||||
|
<slot name="title"></slot>
|
||||||
|
</div>
|
||||||
|
<div class="flex-grow-0 flex items-center h-full">
|
||||||
|
<OneIcon @click="setValue(false)" color="#fff" style="font-size: 24px">close</OneIcon>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</transition>
|
||||||
|
<div class="right-main">
|
||||||
|
<transition appear enter-active-class="animate__slideInDown">
|
||||||
|
<div class="right-main-core animate__animated animate__faster"
|
||||||
|
:style="{'background': backgound}">
|
||||||
|
<slot name="main"></slot>
|
||||||
|
</div>
|
||||||
|
</transition>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import {OneIcon} from '@veypi/one-icon'
|
||||||
|
import {computed, watch} from 'vue'
|
||||||
|
|
||||||
|
let emits = defineEmits<{
|
||||||
|
(e: 'update:modelValue', v: boolean): void
|
||||||
|
}>()
|
||||||
|
let props = withDefaults(defineProps<{
|
||||||
|
isDark?: boolean,
|
||||||
|
modelValue?: boolean
|
||||||
|
}>(), {})
|
||||||
|
|
||||||
|
let backgound = computed(() => {
|
||||||
|
return props.isDark ? '#222' : '#eee'
|
||||||
|
})
|
||||||
|
watch(props, () => {
|
||||||
|
})
|
||||||
|
|
||||||
|
function setValue(b: boolean) {
|
||||||
|
emits('update:modelValue', b)
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.core {
|
||||||
|
position: fixed;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
background: rgba(0, 0, 0, 0.4);
|
||||||
|
z-index: 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
.core-right {
|
||||||
|
position: absolute;
|
||||||
|
right: 0;
|
||||||
|
top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.right-main {
|
||||||
|
width: 100%;
|
||||||
|
height: calc(100% - 50px);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.right-main-core {
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
-webkit-animation-delay: 0.4s;
|
||||||
|
animation-delay: 0.4s;
|
||||||
|
--animate-duration: 400ms;
|
||||||
|
}
|
||||||
|
|
||||||
|
.right-title {
|
||||||
|
width: 100%;
|
||||||
|
height: 50px;
|
||||||
|
line-height: 50px;
|
||||||
|
background: linear-gradient(90deg, #f74d22, #fa9243);
|
||||||
|
}
|
||||||
|
</style>
|
@ -0,0 +1,7 @@
|
|||||||
|
import { toBase64 } from "../tools/encode";
|
||||||
|
import { AuthHeader } from "../types";
|
||||||
|
|
||||||
|
export function generateBasicAuthHeader(username: string, password: string): AuthHeader {
|
||||||
|
const encoded = toBase64(`${username}:${password}`);
|
||||||
|
return `Basic ${encoded}`;
|
||||||
|
}
|
@ -0,0 +1,82 @@
|
|||||||
|
import md5 from "md5";
|
||||||
|
import { ha1Compute } from "../tools/crypto";
|
||||||
|
import { DigestContext, Response } from "../types";
|
||||||
|
|
||||||
|
const NONCE_CHARS = "abcdef0123456789";
|
||||||
|
const NONCE_SIZE = 32;
|
||||||
|
|
||||||
|
export function createDigestContext(username: string, password: string): DigestContext {
|
||||||
|
return { username, password, nc: 0, algorithm: "md5", hasDigestAuth: false };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function generateDigestAuthHeader(options, digest: DigestContext): string {
|
||||||
|
const url = options.url.replace("//", "");
|
||||||
|
const uri = url.indexOf("/") == -1 ? "/" : url.slice(url.indexOf("/"));
|
||||||
|
const method = options.method ? options.method.toUpperCase() : "GET";
|
||||||
|
const qop = /(^|,)\s*auth\s*($|,)/.test(digest.qop) ? "auth" : false;
|
||||||
|
const ncString = `00000000${digest.nc}`.slice(-8);
|
||||||
|
const ha1 = ha1Compute(
|
||||||
|
digest.algorithm,
|
||||||
|
digest.username,
|
||||||
|
digest.realm,
|
||||||
|
digest.password,
|
||||||
|
digest.nonce,
|
||||||
|
digest.cnonce
|
||||||
|
);
|
||||||
|
const ha2 = md5(`${method}:${uri}`);
|
||||||
|
const digestResponse = qop
|
||||||
|
? md5(`${ha1}:${digest.nonce}:${ncString}:${digest.cnonce}:${qop}:${ha2}`)
|
||||||
|
: md5(`${ha1}:${digest.nonce}:${ha2}`);
|
||||||
|
|
||||||
|
const authValues = {
|
||||||
|
username: digest.username,
|
||||||
|
realm: digest.realm,
|
||||||
|
nonce: digest.nonce,
|
||||||
|
uri,
|
||||||
|
qop,
|
||||||
|
response: digestResponse,
|
||||||
|
nc: ncString,
|
||||||
|
cnonce: digest.cnonce,
|
||||||
|
algorithm: digest.algorithm,
|
||||||
|
opaque: digest.opaque
|
||||||
|
};
|
||||||
|
|
||||||
|
const authHeader = [];
|
||||||
|
for (const k in authValues) {
|
||||||
|
if (authValues[k]) {
|
||||||
|
if (k === "qop" || k === "nc" || k === "algorithm") {
|
||||||
|
authHeader.push(`${k}=${authValues[k]}`);
|
||||||
|
} else {
|
||||||
|
authHeader.push(`${k}="${authValues[k]}"`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return `Digest ${authHeader.join(", ")}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function makeNonce(): string {
|
||||||
|
let uid = "";
|
||||||
|
for (let i = 0; i < NONCE_SIZE; ++i) {
|
||||||
|
uid = `${uid}${NONCE_CHARS[Math.floor(Math.random() * NONCE_CHARS.length)]}`;
|
||||||
|
}
|
||||||
|
return uid;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parseDigestAuth(response: Response, _digest: DigestContext): boolean {
|
||||||
|
const authHeader = response.headers["www-authenticate"] || "";
|
||||||
|
if (authHeader.split(/\s/)[0].toLowerCase() !== "digest") {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const re = /([a-z0-9_-]+)=(?:"([^"]+)"|([a-z0-9_-]+))/gi;
|
||||||
|
for (;;) {
|
||||||
|
const match = re.exec(authHeader);
|
||||||
|
if (!match) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
_digest[match[1]] = match[2] || match[3];
|
||||||
|
}
|
||||||
|
_digest.nc += 1;
|
||||||
|
_digest.cnonce = makeNonce();
|
||||||
|
return true;
|
||||||
|
}
|
@ -0,0 +1,36 @@
|
|||||||
|
import { Layerr } from "layerr";
|
||||||
|
import { createDigestContext } from "./digest";
|
||||||
|
import { generateBasicAuthHeader } from "./basic";
|
||||||
|
import { generateTokenAuthHeader } from "./oauth";
|
||||||
|
import { AuthType, ErrorCode, OAuthToken, WebDAVClientContext } from "../types";
|
||||||
|
|
||||||
|
export function setupAuth(
|
||||||
|
context: WebDAVClientContext,
|
||||||
|
username: string,
|
||||||
|
password: string,
|
||||||
|
oauthToken: OAuthToken
|
||||||
|
): void {
|
||||||
|
switch (context.authType) {
|
||||||
|
case AuthType.Digest:
|
||||||
|
context.digest = createDigestContext(username, password);
|
||||||
|
break;
|
||||||
|
case AuthType.None:
|
||||||
|
// Do nothing
|
||||||
|
break;
|
||||||
|
case AuthType.Password:
|
||||||
|
context.headers.Authorization = generateBasicAuthHeader(username, password);
|
||||||
|
break;
|
||||||
|
case AuthType.Token:
|
||||||
|
context.headers.Authorization = generateTokenAuthHeader(oauthToken);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new Layerr(
|
||||||
|
{
|
||||||
|
info: {
|
||||||
|
code: ErrorCode.InvalidAuthType
|
||||||
|
}
|
||||||
|
},
|
||||||
|
`Invalid auth type: ${context.authType}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,5 @@
|
|||||||
|
import { AuthHeader, OAuthToken } from "../types";
|
||||||
|
|
||||||
|
export function generateTokenAuthHeader(token: OAuthToken): AuthHeader {
|
||||||
|
return `${token.token_type} ${token.access_token}`;
|
||||||
|
}
|
@ -0,0 +1,10 @@
|
|||||||
|
const hasArrayBuffer = typeof ArrayBuffer === "function";
|
||||||
|
const { toString: objToString } = Object.prototype;
|
||||||
|
|
||||||
|
// Taken from: https://github.com/fengyuanchen/is-array-buffer/blob/master/src/index.js
|
||||||
|
export function isArrayBuffer(value: any): boolean {
|
||||||
|
return (
|
||||||
|
hasArrayBuffer &&
|
||||||
|
(value instanceof ArrayBuffer || objToString.call(value) === "[object ArrayBuffer]")
|
||||||
|
);
|
||||||
|
}
|
@ -0,0 +1,8 @@
|
|||||||
|
export function isBuffer(value: any): boolean {
|
||||||
|
return (
|
||||||
|
value != null &&
|
||||||
|
value.constructor != null &&
|
||||||
|
typeof value.constructor.isBuffer === "function" &&
|
||||||
|
value.constructor.isBuffer(value)
|
||||||
|
);
|
||||||
|
}
|
@ -0,0 +1,10 @@
|
|||||||
|
import HotPatcher from "hot-patcher";
|
||||||
|
|
||||||
|
let __patcher: HotPatcher = null;
|
||||||
|
|
||||||
|
export function getPatcher(): HotPatcher {
|
||||||
|
if (!__patcher) {
|
||||||
|
__patcher = new HotPatcher();
|
||||||
|
}
|
||||||
|
return __patcher;
|
||||||
|
}
|
@ -0,0 +1,114 @@
|
|||||||
|
import Stream from "stream";
|
||||||
|
import { extractURLPath } from "./tools/url";
|
||||||
|
import { setupAuth } from "./auth/index";
|
||||||
|
import { copyFile } from "./operations/copyFile";
|
||||||
|
import { createDirectory } from "./operations/createDirectory";
|
||||||
|
import { createReadStream, createWriteStream } from "./operations/createStream";
|
||||||
|
import { customRequest } from "./operations/customRequest";
|
||||||
|
import { deleteFile } from "./operations/deleteFile";
|
||||||
|
import { exists } from "./operations/exists";
|
||||||
|
import { getDirectoryContents } from "./operations/directoryContents";
|
||||||
|
import { getFileContents, getFileDownloadLink } from "./operations/getFileContents";
|
||||||
|
import { lock, unlock } from "./operations/lock";
|
||||||
|
import { getQuota } from "./operations/getQuota";
|
||||||
|
import { getStat } from "./operations/stat";
|
||||||
|
import { moveFile } from "./operations/moveFile";
|
||||||
|
import { getFileUploadLink, putFileContents } from "./operations/putFileContents";
|
||||||
|
import {
|
||||||
|
AuthType,
|
||||||
|
BufferLike,
|
||||||
|
CreateReadStreamOptions,
|
||||||
|
CreateWriteStreamCallback,
|
||||||
|
CreateWriteStreamOptions,
|
||||||
|
GetDirectoryContentsOptions,
|
||||||
|
GetFileContentsOptions,
|
||||||
|
GetQuotaOptions,
|
||||||
|
Headers,
|
||||||
|
LockOptions,
|
||||||
|
PutFileContentsOptions,
|
||||||
|
RequestOptionsCustom,
|
||||||
|
StatOptions,
|
||||||
|
WebDAVClient,
|
||||||
|
WebDAVClientContext,
|
||||||
|
WebDAVClientOptions,
|
||||||
|
WebDAVMethodOptions
|
||||||
|
} from "./types";
|
||||||
|
|
||||||
|
const DEFAULT_CONTACT_HREF =
|
||||||
|
"https://github.com/perry-mitchell/webdav-client/blob/master/LOCK_CONTACT.md";
|
||||||
|
|
||||||
|
export function createClient(remoteURL: string, options: WebDAVClientOptions = {}): WebDAVClient {
|
||||||
|
const {
|
||||||
|
authType: authTypeRaw = null,
|
||||||
|
contactHref = DEFAULT_CONTACT_HREF,
|
||||||
|
headers = {},
|
||||||
|
httpAgent,
|
||||||
|
httpsAgent,
|
||||||
|
maxBodyLength,
|
||||||
|
maxContentLength,
|
||||||
|
password,
|
||||||
|
token,
|
||||||
|
username,
|
||||||
|
withCredentials
|
||||||
|
} = options;
|
||||||
|
let authType = authTypeRaw;
|
||||||
|
if (!authType) {
|
||||||
|
authType = username || password ? AuthType.Password : AuthType.None;
|
||||||
|
}
|
||||||
|
const context: WebDAVClientContext = {
|
||||||
|
authType,
|
||||||
|
contactHref,
|
||||||
|
headers: Object.assign({}, headers),
|
||||||
|
httpAgent,
|
||||||
|
httpsAgent,
|
||||||
|
maxBodyLength,
|
||||||
|
maxContentLength,
|
||||||
|
remotePath: extractURLPath(remoteURL),
|
||||||
|
remoteURL,
|
||||||
|
password,
|
||||||
|
token,
|
||||||
|
username,
|
||||||
|
withCredentials
|
||||||
|
};
|
||||||
|
setupAuth(context, username, password, token);
|
||||||
|
return {
|
||||||
|
copyFile: (filename: string, destination: string, options?: WebDAVMethodOptions) =>
|
||||||
|
copyFile(context, filename, destination, options),
|
||||||
|
createDirectory: (path: string, options?: WebDAVMethodOptions) =>
|
||||||
|
createDirectory(context, path, options),
|
||||||
|
createReadStream: (filename: string, options?: CreateReadStreamOptions) =>
|
||||||
|
createReadStream(context, filename, options),
|
||||||
|
createWriteStream: (
|
||||||
|
filename: string,
|
||||||
|
options?: CreateWriteStreamOptions,
|
||||||
|
callback?: CreateWriteStreamCallback
|
||||||
|
) => createWriteStream(context, filename, options, callback),
|
||||||
|
customRequest: (path: string, requestOptions: RequestOptionsCustom) =>
|
||||||
|
customRequest(context, path, requestOptions),
|
||||||
|
deleteFile: (filename: string, options?: WebDAVMethodOptions) =>
|
||||||
|
deleteFile(context, filename, options),
|
||||||
|
exists: (path: string, options?: WebDAVMethodOptions) => exists(context, path, options),
|
||||||
|
getDirectoryContents: (path: string, options?: GetDirectoryContentsOptions) =>
|
||||||
|
getDirectoryContents(context, path, options),
|
||||||
|
getFileContents: (filename: string, options?: GetFileContentsOptions) =>
|
||||||
|
getFileContents(context, filename, options),
|
||||||
|
getFileDownloadLink: (filename: string) => getFileDownloadLink(context, filename),
|
||||||
|
getFileUploadLink: (filename: string) => getFileUploadLink(context, filename),
|
||||||
|
getHeaders: () => Object.assign({}, context.headers),
|
||||||
|
getQuota: (options?: GetQuotaOptions) => getQuota(context, options),
|
||||||
|
lock: (path: string, options?: LockOptions) => lock(context, path, options),
|
||||||
|
moveFile: (filename: string, destinationFilename: string, options?: WebDAVMethodOptions) =>
|
||||||
|
moveFile(context, filename, destinationFilename, options),
|
||||||
|
putFileContents: (
|
||||||
|
filename: string,
|
||||||
|
data: string | BufferLike | Stream.Readable,
|
||||||
|
options?: PutFileContentsOptions
|
||||||
|
) => putFileContents(context, filename, data, options),
|
||||||
|
setHeaders: (headers: Headers) => {
|
||||||
|
context.headers = Object.assign({}, headers);
|
||||||
|
},
|
||||||
|
stat: (path: string, options?: StatOptions) => getStat(context, path, options),
|
||||||
|
unlock: (path: string, token: string, options?: WebDAVMethodOptions) =>
|
||||||
|
unlock(context, path, token, options)
|
||||||
|
};
|
||||||
|
}
|
@ -0,0 +1,5 @@
|
|||||||
|
export { createClient } from "./factory";
|
||||||
|
export { getPatcher } from "./compat/patcher";
|
||||||
|
export * from "./types";
|
||||||
|
|
||||||
|
export { parseStat, parseXML } from "./tools/dav";
|
@ -0,0 +1,26 @@
|
|||||||
|
import { joinURL } from "../tools/url";
|
||||||
|
import { encodePath } from "../tools/path";
|
||||||
|
import { request, prepareRequestOptions } from "../request";
|
||||||
|
import { handleResponseCode } from "../response";
|
||||||
|
import { WebDAVClientContext, WebDAVMethodOptions } from "../types";
|
||||||
|
|
||||||
|
export async function copyFile(
|
||||||
|
context: WebDAVClientContext,
|
||||||
|
filename: string,
|
||||||
|
destination: string,
|
||||||
|
options: WebDAVMethodOptions = {}
|
||||||
|
): Promise<void> {
|
||||||
|
const requestOptions = prepareRequestOptions(
|
||||||
|
{
|
||||||
|
url: joinURL(context.remoteURL, encodePath(filename)),
|
||||||
|
method: "COPY",
|
||||||
|
headers: {
|
||||||
|
Destination: joinURL(context.remoteURL, encodePath(destination))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
context,
|
||||||
|
options
|
||||||
|
);
|
||||||
|
const response = await request(requestOptions);
|
||||||
|
handleResponseCode(context, response);
|
||||||
|
}
|
@ -0,0 +1,81 @@
|
|||||||
|
import { joinURL } from "../tools/url";
|
||||||
|
import { encodePath, getAllDirectories, normalisePath } from "../tools/path";
|
||||||
|
import { request, prepareRequestOptions } from "../request";
|
||||||
|
import { handleResponseCode } from "../response";
|
||||||
|
import { getStat } from "./stat";
|
||||||
|
import { CreateDirectoryOptions, FileStat, WebDAVClientContext, WebDAVClientError } from "../types";
|
||||||
|
|
||||||
|
export async function createDirectory(
|
||||||
|
context: WebDAVClientContext,
|
||||||
|
dirPath: string,
|
||||||
|
options: CreateDirectoryOptions = {}
|
||||||
|
): Promise<void> {
|
||||||
|
if (options.recursive === true) return createDirectoryRecursively(context, dirPath, options);
|
||||||
|
const requestOptions = prepareRequestOptions(
|
||||||
|
{
|
||||||
|
url: joinURL(context.remoteURL, ensureCollectionPath(encodePath(dirPath))),
|
||||||
|
method: "MKCOL"
|
||||||
|
},
|
||||||
|
context,
|
||||||
|
options
|
||||||
|
);
|
||||||
|
const response = await request(requestOptions);
|
||||||
|
handleResponseCode(context, response);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ensure the path is a proper "collection" path by ensuring it has a trailing "/".
|
||||||
|
* The proper format of collection according to the specification does contain the trailing slash.
|
||||||
|
* http://www.webdav.org/specs/rfc4918.html#rfc.section.5.2
|
||||||
|
* @param path Path of the collection
|
||||||
|
* @return string Path of the collection with appended trailing "/" in case the `path` does not have it.
|
||||||
|
*/
|
||||||
|
function ensureCollectionPath(path: string): string {
|
||||||
|
if (!path.endsWith("/")) {
|
||||||
|
return path + "/";
|
||||||
|
}
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createDirectoryRecursively(
|
||||||
|
context: WebDAVClientContext,
|
||||||
|
dirPath: string,
|
||||||
|
options: CreateDirectoryOptions = {}
|
||||||
|
): Promise<void> {
|
||||||
|
const paths = getAllDirectories(normalisePath(dirPath));
|
||||||
|
paths.sort((a, b) => {
|
||||||
|
if (a.length > b.length) {
|
||||||
|
return 1;
|
||||||
|
} else if (b.length > a.length) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
});
|
||||||
|
let creating: boolean = false;
|
||||||
|
for (const testPath of paths) {
|
||||||
|
if (creating) {
|
||||||
|
await createDirectory(context, testPath, {
|
||||||
|
...options,
|
||||||
|
recursive: false
|
||||||
|
});
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const testStat = (await getStat(context, testPath)) as FileStat;
|
||||||
|
if (testStat.type !== "directory") {
|
||||||
|
throw new Error(`Path includes a file: ${dirPath}`);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
const error = err as WebDAVClientError;
|
||||||
|
if (error.status === 404) {
|
||||||
|
creating = true;
|
||||||
|
await createDirectory(context, testPath, {
|
||||||
|
...options,
|
||||||
|
recursive: false
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,109 @@
|
|||||||
|
import Stream from "stream";
|
||||||
|
import { joinURL } from "../tools/url";
|
||||||
|
import { encodePath } from "../tools/path";
|
||||||
|
import { request, prepareRequestOptions } from "../request";
|
||||||
|
import { handleResponseCode } from "../response";
|
||||||
|
import {
|
||||||
|
CreateReadStreamOptions,
|
||||||
|
CreateWriteStreamCallback,
|
||||||
|
CreateWriteStreamOptions,
|
||||||
|
Headers,
|
||||||
|
WebDAVClientContext,
|
||||||
|
WebDAVClientError
|
||||||
|
} from "../types";
|
||||||
|
|
||||||
|
const NOOP = () => {};
|
||||||
|
|
||||||
|
export function createReadStream(
|
||||||
|
context: WebDAVClientContext,
|
||||||
|
filePath: string,
|
||||||
|
options: CreateReadStreamOptions = {}
|
||||||
|
): Stream.Readable {
|
||||||
|
const PassThroughStream = Stream.PassThrough;
|
||||||
|
const outStream = new PassThroughStream();
|
||||||
|
getFileStream(context, filePath, options)
|
||||||
|
.then(stream => {
|
||||||
|
stream.pipe(outStream);
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
outStream.emit("error", err);
|
||||||
|
});
|
||||||
|
return outStream;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createWriteStream(
|
||||||
|
context: WebDAVClientContext,
|
||||||
|
filePath: string,
|
||||||
|
options: CreateWriteStreamOptions = {},
|
||||||
|
callback: CreateWriteStreamCallback = NOOP
|
||||||
|
): Stream.Writable {
|
||||||
|
const PassThroughStream = Stream.PassThrough;
|
||||||
|
const writeStream = new PassThroughStream();
|
||||||
|
const headers = {};
|
||||||
|
if (options.overwrite === false) {
|
||||||
|
headers["If-None-Match"] = "*";
|
||||||
|
}
|
||||||
|
const requestOptions = prepareRequestOptions(
|
||||||
|
{
|
||||||
|
url: joinURL(context.remoteURL, encodePath(filePath)),
|
||||||
|
method: "PUT",
|
||||||
|
headers,
|
||||||
|
data: writeStream,
|
||||||
|
maxRedirects: 0
|
||||||
|
},
|
||||||
|
context,
|
||||||
|
options
|
||||||
|
);
|
||||||
|
request(requestOptions)
|
||||||
|
.then(response => handleResponseCode(context, response))
|
||||||
|
.then(response => {
|
||||||
|
// Fire callback asynchronously to avoid errors
|
||||||
|
setTimeout(() => {
|
||||||
|
callback(response);
|
||||||
|
}, 0);
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
writeStream.emit("error", err);
|
||||||
|
});
|
||||||
|
return writeStream;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getFileStream(
|
||||||
|
context: WebDAVClientContext,
|
||||||
|
filePath: string,
|
||||||
|
options: CreateReadStreamOptions = {}
|
||||||
|
): Promise<Stream.Readable> {
|
||||||
|
const headers: Headers = {};
|
||||||
|
if (typeof options.range === "object" && typeof options.range.start === "number") {
|
||||||
|
let rangeHeader = `bytes=${options.range.start}-`;
|
||||||
|
if (typeof options.range.end === "number") {
|
||||||
|
rangeHeader = `${rangeHeader}${options.range.end}`;
|
||||||
|
}
|
||||||
|
headers.Range = rangeHeader;
|
||||||
|
}
|
||||||
|
const requestOptions = prepareRequestOptions(
|
||||||
|
{
|
||||||
|
url: joinURL(context.remoteURL, encodePath(filePath)),
|
||||||
|
method: "GET",
|
||||||
|
headers,
|
||||||
|
responseType: "stream"
|
||||||
|
},
|
||||||
|
context,
|
||||||
|
options
|
||||||
|
);
|
||||||
|
const response = await request(requestOptions);
|
||||||
|
handleResponseCode(context, response);
|
||||||
|
if (headers.Range && response.status !== 206) {
|
||||||
|
const responseError: WebDAVClientError = new Error(
|
||||||
|
`Invalid response code for partial request: ${response.status}`
|
||||||
|
);
|
||||||
|
responseError.status = response.status;
|
||||||
|
throw responseError;
|
||||||
|
}
|
||||||
|
if (options.callback) {
|
||||||
|
setTimeout(() => {
|
||||||
|
options.callback(response);
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
|
return response.data as Stream.Readable;
|
||||||
|
}
|
@ -0,0 +1,19 @@
|
|||||||
|
import { joinURL } from "../tools/url";
|
||||||
|
import { encodePath } from "../tools/path";
|
||||||
|
import { request, prepareRequestOptions } from "../request";
|
||||||
|
import { handleResponseCode } from "../response";
|
||||||
|
import { RequestOptionsCustom, Response, WebDAVClientContext } from "../types";
|
||||||
|
|
||||||
|
export async function customRequest(
|
||||||
|
context: WebDAVClientContext,
|
||||||
|
remotePath: string,
|
||||||
|
requestOptions: RequestOptionsCustom
|
||||||
|
): Promise<Response> {
|
||||||
|
if (!requestOptions.url) {
|
||||||
|
requestOptions.url = joinURL(context.remoteURL, encodePath(remotePath));
|
||||||
|
}
|
||||||
|
const finalOptions = prepareRequestOptions(requestOptions, context, {});
|
||||||
|
const response = await request(finalOptions);
|
||||||
|
handleResponseCode(context, response);
|
||||||
|
return response;
|
||||||
|
}
|
@ -0,0 +1,22 @@
|
|||||||
|
import { joinURL } from "../tools/url";
|
||||||
|
import { encodePath } from "../tools/path";
|
||||||
|
import { request, prepareRequestOptions } from "../request";
|
||||||
|
import { handleResponseCode } from "../response";
|
||||||
|
import { WebDAVClientContext, WebDAVMethodOptions } from "../types";
|
||||||
|
|
||||||
|
export async function deleteFile(
|
||||||
|
context: WebDAVClientContext,
|
||||||
|
filename: string,
|
||||||
|
options: WebDAVMethodOptions = {}
|
||||||
|
): Promise<void> {
|
||||||
|
const requestOptions = prepareRequestOptions(
|
||||||
|
{
|
||||||
|
url: joinURL(context.remoteURL, encodePath(filename)),
|
||||||
|
method: "DELETE"
|
||||||
|
},
|
||||||
|
context,
|
||||||
|
options
|
||||||
|
);
|
||||||
|
const response = await request(requestOptions);
|
||||||
|
handleResponseCode(context, response);
|
||||||
|
}
|
@ -0,0 +1,78 @@
|
|||||||
|
import pathPosix from "path-posix";
|
||||||
|
import { joinURL, normaliseHREF } from "../tools/url";
|
||||||
|
import { encodePath, normalisePath } from "../tools/path";
|
||||||
|
import { parseXML, prepareFileFromProps } from "../tools/dav";
|
||||||
|
import { request, prepareRequestOptions } from "../request";
|
||||||
|
import { handleResponseCode, processGlobFilter, processResponsePayload } from "../response";
|
||||||
|
import {
|
||||||
|
DAVResult,
|
||||||
|
FileStat,
|
||||||
|
GetDirectoryContentsOptions,
|
||||||
|
ResponseDataDetailed,
|
||||||
|
WebDAVClientContext
|
||||||
|
} from "../types";
|
||||||
|
|
||||||
|
export async function getDirectoryContents(
|
||||||
|
context: WebDAVClientContext,
|
||||||
|
remotePath: string,
|
||||||
|
options: GetDirectoryContentsOptions = {}
|
||||||
|
): Promise<Array<FileStat> | ResponseDataDetailed<Array<FileStat>>> {
|
||||||
|
const requestOptions = prepareRequestOptions(
|
||||||
|
{
|
||||||
|
url: joinURL(context.remoteURL, encodePath(remotePath), "/"),
|
||||||
|
method: "PROPFIND",
|
||||||
|
headers: {
|
||||||
|
Accept: "text/plain",
|
||||||
|
Depth: options.deep ? "infinity" : "1"
|
||||||
|
},
|
||||||
|
responseType: "text"
|
||||||
|
},
|
||||||
|
context,
|
||||||
|
options
|
||||||
|
);
|
||||||
|
const response = await request(requestOptions);
|
||||||
|
handleResponseCode(context, response);
|
||||||
|
const davResp = await parseXML(response.data as string);
|
||||||
|
let files = getDirectoryFiles(davResp, context.remotePath, remotePath, options.details);
|
||||||
|
if (options.glob) {
|
||||||
|
files = processGlobFilter(files, options.glob);
|
||||||
|
}
|
||||||
|
return processResponsePayload(response, files, options.details);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDirectoryFiles(
|
||||||
|
result: DAVResult,
|
||||||
|
serverBasePath: string,
|
||||||
|
requestPath: string,
|
||||||
|
isDetailed: boolean = false
|
||||||
|
): Array<FileStat> {
|
||||||
|
const serverBase = pathPosix.join(serverBasePath, "/");
|
||||||
|
// Extract the response items (directory contents)
|
||||||
|
const {
|
||||||
|
multistatus: { response: responseItems }
|
||||||
|
} = result;
|
||||||
|
return (
|
||||||
|
responseItems
|
||||||
|
// Map all items to a consistent output structure (results)
|
||||||
|
.map(item => {
|
||||||
|
// HREF is the file path (in full)
|
||||||
|
const href = normaliseHREF(item.href);
|
||||||
|
// Each item should contain a stat object
|
||||||
|
const {
|
||||||
|
propstat: { prop: props }
|
||||||
|
} = item;
|
||||||
|
// Process the true full filename (minus the base server path)
|
||||||
|
const filename =
|
||||||
|
serverBase === "/"
|
||||||
|
? decodeURIComponent(normalisePath(href))
|
||||||
|
: decodeURIComponent(normalisePath(pathPosix.relative(serverBase, href)));
|
||||||
|
return prepareFileFromProps(props, filename, isDetailed);
|
||||||
|
})
|
||||||
|
// Filter out the item pointing to the current directory (not needed)
|
||||||
|
.filter(
|
||||||
|
item =>
|
||||||
|
item.basename &&
|
||||||
|
(item.type === "file" || item.filename !== requestPath.replace(/\/$/, ""))
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
@ -0,0 +1,18 @@
|
|||||||
|
import { getStat } from "./stat";
|
||||||
|
import { WebDAVClientContext, WebDAVMethodOptions } from "../types";
|
||||||
|
|
||||||
|
export async function exists(
|
||||||
|
context: WebDAVClientContext,
|
||||||
|
remotePath: string,
|
||||||
|
options: WebDAVMethodOptions = {}
|
||||||
|
): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
await getStat(context, remotePath, options);
|
||||||
|
return true;
|
||||||
|
} catch (err) {
|
||||||
|
if (err.status === 404) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,102 @@
|
|||||||
|
import { Layerr } from "layerr";
|
||||||
|
import { joinURL } from "../tools/url";
|
||||||
|
import { encodePath } from "../tools/path";
|
||||||
|
import { fromBase64 } from "../tools/encode";
|
||||||
|
import { request, prepareRequestOptions } from "../request";
|
||||||
|
import { handleResponseCode, processResponsePayload } from "../response";
|
||||||
|
import {
|
||||||
|
AuthType,
|
||||||
|
BufferLike,
|
||||||
|
ErrorCode,
|
||||||
|
GetFileContentsOptions,
|
||||||
|
ResponseDataDetailed,
|
||||||
|
WebDAVClientContext
|
||||||
|
} from "../types";
|
||||||
|
|
||||||
|
const TRANSFORM_RETAIN_FORMAT = (v: any) => v;
|
||||||
|
|
||||||
|
export async function getFileContents(
|
||||||
|
context: WebDAVClientContext,
|
||||||
|
filePath: string,
|
||||||
|
options: GetFileContentsOptions = {}
|
||||||
|
): Promise<BufferLike | string | ResponseDataDetailed<BufferLike | string>> {
|
||||||
|
const { format = "binary" } = options;
|
||||||
|
if (format !== "binary" && format !== "text") {
|
||||||
|
throw new Layerr(
|
||||||
|
{
|
||||||
|
info: {
|
||||||
|
code: ErrorCode.InvalidOutputFormat
|
||||||
|
}
|
||||||
|
},
|
||||||
|
`Invalid output format: ${format}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return format === "text"
|
||||||
|
? getFileContentsString(context, filePath, options)
|
||||||
|
: getFileContentsBuffer(context, filePath, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getFileContentsBuffer(
|
||||||
|
context: WebDAVClientContext,
|
||||||
|
filePath: string,
|
||||||
|
options: GetFileContentsOptions = {}
|
||||||
|
): Promise<BufferLike | ResponseDataDetailed<BufferLike>> {
|
||||||
|
const requestOptions = prepareRequestOptions(
|
||||||
|
{
|
||||||
|
url: joinURL(context.remoteURL, encodePath(filePath)),
|
||||||
|
method: "GET",
|
||||||
|
responseType: "arraybuffer"
|
||||||
|
},
|
||||||
|
context,
|
||||||
|
options
|
||||||
|
);
|
||||||
|
const response = await request(requestOptions);
|
||||||
|
handleResponseCode(context, response);
|
||||||
|
return processResponsePayload(response, response.data as BufferLike, options.details);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getFileContentsString(
|
||||||
|
context: WebDAVClientContext,
|
||||||
|
filePath: string,
|
||||||
|
options: GetFileContentsOptions = {}
|
||||||
|
): Promise<string | ResponseDataDetailed<string>> {
|
||||||
|
const requestOptions = prepareRequestOptions(
|
||||||
|
{
|
||||||
|
url: joinURL(context.remoteURL, encodePath(filePath)),
|
||||||
|
method: "GET",
|
||||||
|
responseType: "text",
|
||||||
|
transformResponse: [TRANSFORM_RETAIN_FORMAT]
|
||||||
|
},
|
||||||
|
context,
|
||||||
|
options
|
||||||
|
);
|
||||||
|
const response = await request(requestOptions);
|
||||||
|
handleResponseCode(context, response);
|
||||||
|
return processResponsePayload(response, response.data as string, options.details);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getFileDownloadLink(context: WebDAVClientContext, filePath: string): string {
|
||||||
|
let url = joinURL(context.remoteURL, encodePath(filePath));
|
||||||
|
const protocol = /^https:/i.test(url) ? "https" : "http";
|
||||||
|
switch (context.authType) {
|
||||||
|
case AuthType.None:
|
||||||
|
// Do nothing
|
||||||
|
break;
|
||||||
|
case AuthType.Password: {
|
||||||
|
const authPart = context.headers.Authorization.replace(/^Basic /i, "").trim();
|
||||||
|
const authContents = fromBase64(authPart);
|
||||||
|
url = url.replace(/^https?:\/\//, `${protocol}://${authContents}@`);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
throw new Layerr(
|
||||||
|
{
|
||||||
|
info: {
|
||||||
|
code: ErrorCode.LinkUnsupportedAuthType
|
||||||
|
}
|
||||||
|
},
|
||||||
|
`Unsupported auth type for file link: ${context.authType}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return url;
|
||||||
|
}
|
@ -0,0 +1,30 @@
|
|||||||
|
import { prepareRequestOptions, request } from "../request";
|
||||||
|
import { handleResponseCode, processResponsePayload } from "../response";
|
||||||
|
import { parseXML } from "../tools/dav";
|
||||||
|
import { joinURL } from "../tools/url";
|
||||||
|
import { parseQuota } from "../tools/quota";
|
||||||
|
import { DiskQuota, GetQuotaOptions, ResponseDataDetailed, WebDAVClientContext } from "../types";
|
||||||
|
|
||||||
|
export async function getQuota(
|
||||||
|
context: WebDAVClientContext,
|
||||||
|
options: GetQuotaOptions = {}
|
||||||
|
): Promise<DiskQuota | null | ResponseDataDetailed<DiskQuota | null>> {
|
||||||
|
const requestOptions = prepareRequestOptions(
|
||||||
|
{
|
||||||
|
url: joinURL(context.remoteURL, "/"),
|
||||||
|
method: "PROPFIND",
|
||||||
|
headers: {
|
||||||
|
Accept: "text/plain",
|
||||||
|
Depth: "0"
|
||||||
|
},
|
||||||
|
responseType: "text"
|
||||||
|
},
|
||||||
|
context,
|
||||||
|
options
|
||||||
|
);
|
||||||
|
const response = await request(requestOptions);
|
||||||
|
handleResponseCode(context, response);
|
||||||
|
const result = await parseXML(response.data as string);
|
||||||
|
const quota = parseQuota(result);
|
||||||
|
return processResponsePayload(response, quota, options.details);
|
||||||
|
}
|
@ -0,0 +1,79 @@
|
|||||||
|
import nestedProp from "nested-property";
|
||||||
|
import { joinURL } from "../tools/url";
|
||||||
|
import { encodePath } from "../tools/path";
|
||||||
|
import { generateLockXML, parseGenericResponse } from "../tools/xml";
|
||||||
|
import { request, prepareRequestOptions } from "../request";
|
||||||
|
import { createErrorFromResponse, handleResponseCode } from "../response";
|
||||||
|
import {
|
||||||
|
Headers,
|
||||||
|
LockOptions,
|
||||||
|
LockResponse,
|
||||||
|
WebDAVClientContext,
|
||||||
|
WebDAVMethodOptions
|
||||||
|
} from "../types";
|
||||||
|
|
||||||
|
const DEFAULT_TIMEOUT = "Infinite, Second-4100000000";
|
||||||
|
|
||||||
|
export async function lock(
|
||||||
|
context: WebDAVClientContext,
|
||||||
|
path: string,
|
||||||
|
options: LockOptions = {}
|
||||||
|
): Promise<LockResponse> {
|
||||||
|
const { refreshToken, timeout = DEFAULT_TIMEOUT } = options;
|
||||||
|
const headers: Headers = {
|
||||||
|
Accept: "text/plain,application/xml",
|
||||||
|
Timeout: timeout
|
||||||
|
};
|
||||||
|
if (refreshToken) {
|
||||||
|
headers.If = refreshToken;
|
||||||
|
}
|
||||||
|
const requestOptions = prepareRequestOptions(
|
||||||
|
{
|
||||||
|
url: joinURL(context.remoteURL, encodePath(path)),
|
||||||
|
method: "LOCK",
|
||||||
|
headers,
|
||||||
|
data: generateLockXML(context.contactHref),
|
||||||
|
responseType: "text"
|
||||||
|
},
|
||||||
|
context,
|
||||||
|
options
|
||||||
|
);
|
||||||
|
const response = await request(requestOptions);
|
||||||
|
handleResponseCode(context, response);
|
||||||
|
const lockPayload = parseGenericResponse(response.data as string);
|
||||||
|
const token = nestedProp.get(lockPayload, "prop.lockdiscovery.activelock.locktoken.href");
|
||||||
|
const serverTimeout = nestedProp.get(lockPayload, "prop.lockdiscovery.activelock.timeout");
|
||||||
|
if (!token) {
|
||||||
|
const err = createErrorFromResponse(response, "No lock token received: ");
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
token,
|
||||||
|
serverTimeout
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function unlock(
|
||||||
|
context: WebDAVClientContext,
|
||||||
|
path: string,
|
||||||
|
token: string,
|
||||||
|
options: WebDAVMethodOptions = {}
|
||||||
|
): Promise<void> {
|
||||||
|
const requestOptions = prepareRequestOptions(
|
||||||
|
{
|
||||||
|
url: joinURL(context.remoteURL, encodePath(path)),
|
||||||
|
method: "UNLOCK",
|
||||||
|
headers: {
|
||||||
|
"Lock-Token": token
|
||||||
|
}
|
||||||
|
},
|
||||||
|
context,
|
||||||
|
options
|
||||||
|
);
|
||||||
|
const response = await request(requestOptions);
|
||||||
|
handleResponseCode(context, response);
|
||||||
|
if (response.status !== 204 && response.status !== 200) {
|
||||||
|
const err = createErrorFromResponse(response);
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,26 @@
|
|||||||
|
import { joinURL } from "../tools/url";
|
||||||
|
import { encodePath } from "../tools/path";
|
||||||
|
import { request, prepareRequestOptions } from "../request";
|
||||||
|
import { handleResponseCode } from "../response";
|
||||||
|
import { WebDAVClientContext, WebDAVMethodOptions } from "../types";
|
||||||
|
|
||||||
|
export async function moveFile(
|
||||||
|
context: WebDAVClientContext,
|
||||||
|
filename: string,
|
||||||
|
destination: string,
|
||||||
|
options: WebDAVMethodOptions = {}
|
||||||
|
): Promise<void> {
|
||||||
|
const requestOptions = prepareRequestOptions(
|
||||||
|
{
|
||||||
|
url: joinURL(context.remoteURL, encodePath(filename)),
|
||||||
|
method: "MOVE",
|
||||||
|
headers: {
|
||||||
|
Destination: joinURL(context.remoteURL, encodePath(destination))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
context,
|
||||||
|
options
|
||||||
|
);
|
||||||
|
const response = await request(requestOptions);
|
||||||
|
handleResponseCode(context, response);
|
||||||
|
}
|
@ -0,0 +1,94 @@
|
|||||||
|
import { Layerr } from "layerr";
|
||||||
|
import Stream from "stream";
|
||||||
|
import { fromBase64 } from "../tools/encode";
|
||||||
|
import { joinURL } from "../tools/url";
|
||||||
|
import { encodePath } from "../tools/path";
|
||||||
|
import { request, prepareRequestOptions } from "../request";
|
||||||
|
import { handleResponseCode } from "../response";
|
||||||
|
import { calculateDataLength } from "../tools/size";
|
||||||
|
import {
|
||||||
|
AuthType,
|
||||||
|
BufferLike,
|
||||||
|
ErrorCode,
|
||||||
|
Headers,
|
||||||
|
PutFileContentsOptions,
|
||||||
|
WebDAVClientContext,
|
||||||
|
WebDAVClientError
|
||||||
|
} from "../types";
|
||||||
|
|
||||||
|
declare var WEB: boolean;
|
||||||
|
|
||||||
|
export async function putFileContents(
|
||||||
|
context: WebDAVClientContext,
|
||||||
|
filePath: string,
|
||||||
|
data: string | BufferLike | Stream.Readable,
|
||||||
|
options: PutFileContentsOptions = {}
|
||||||
|
): Promise<boolean> {
|
||||||
|
const { contentLength = true, overwrite = true } = options;
|
||||||
|
const headers: Headers = {
|
||||||
|
"Content-Type": "application/octet-stream"
|
||||||
|
};
|
||||||
|
if (typeof WEB === "undefined") {
|
||||||
|
// Skip, no content-length
|
||||||
|
} else if (contentLength === false) {
|
||||||
|
// Skip, disabled
|
||||||
|
} else if (typeof contentLength === "number") {
|
||||||
|
headers["Content-Length"] = `${contentLength}`;
|
||||||
|
} else {
|
||||||
|
headers["Content-Length"] = `${calculateDataLength(data as string | BufferLike)}`;
|
||||||
|
}
|
||||||
|
if (!overwrite) {
|
||||||
|
headers["If-None-Match"] = "*";
|
||||||
|
}
|
||||||
|
const requestOptions = prepareRequestOptions(
|
||||||
|
{
|
||||||
|
url: joinURL(context.remoteURL, encodePath(filePath)),
|
||||||
|
method: "PUT",
|
||||||
|
headers,
|
||||||
|
data
|
||||||
|
},
|
||||||
|
context,
|
||||||
|
options
|
||||||
|
);
|
||||||
|
const response = await request(requestOptions);
|
||||||
|
try {
|
||||||
|
handleResponseCode(context, response);
|
||||||
|
} catch (err) {
|
||||||
|
const error = err as WebDAVClientError;
|
||||||
|
if (error.status === 412 && !overwrite) {
|
||||||
|
return false;
|
||||||
|
} else {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getFileUploadLink(context: WebDAVClientContext, filePath: string): string {
|
||||||
|
let url: string = `${joinURL(
|
||||||
|
context.remoteURL,
|
||||||
|
encodePath(filePath)
|
||||||
|
)}?Content-Type=application/octet-stream`;
|
||||||
|
const protocol = /^https:/i.test(url) ? "https" : "http";
|
||||||
|
switch (context.authType) {
|
||||||
|
case AuthType.None:
|
||||||
|
// Do nothing
|
||||||
|
break;
|
||||||
|
case AuthType.Password: {
|
||||||
|
const authPart = context.headers.Authorization.replace(/^Basic /i, "").trim();
|
||||||
|
const authContents = fromBase64(authPart);
|
||||||
|
url = url.replace(/^https?:\/\//, `${protocol}://${authContents}@`);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
throw new Layerr(
|
||||||
|
{
|
||||||
|
info: {
|
||||||
|
code: ErrorCode.LinkUnsupportedAuthType
|
||||||
|
}
|
||||||
|
},
|
||||||
|
`Unsupported auth type for file link: ${context.authType}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return url;
|
||||||
|
}
|
@ -0,0 +1,32 @@
|
|||||||
|
import { parseStat, parseXML } from "../tools/dav";
|
||||||
|
import { joinURL } from "../tools/url";
|
||||||
|
import { encodePath } from "../tools/path";
|
||||||
|
import { request, prepareRequestOptions } from "../request";
|
||||||
|
import { handleResponseCode, processResponsePayload } from "../response";
|
||||||
|
import { FileStat, ResponseDataDetailed, StatOptions, WebDAVClientContext } from "../types";
|
||||||
|
|
||||||
|
export async function getStat(
|
||||||
|
context: WebDAVClientContext,
|
||||||
|
filename: string,
|
||||||
|
options: StatOptions = {}
|
||||||
|
): Promise<FileStat | ResponseDataDetailed<FileStat>> {
|
||||||
|
const { details: isDetailed = false } = options;
|
||||||
|
const requestOptions = prepareRequestOptions(
|
||||||
|
{
|
||||||
|
url: joinURL(context.remoteURL, encodePath(filename)),
|
||||||
|
method: "PROPFIND",
|
||||||
|
headers: {
|
||||||
|
Accept: "text/plain,application/xml",
|
||||||
|
Depth: "0"
|
||||||
|
},
|
||||||
|
responseType: "text"
|
||||||
|
},
|
||||||
|
context,
|
||||||
|
options
|
||||||
|
);
|
||||||
|
const response = await request(requestOptions);
|
||||||
|
handleResponseCode(context, response);
|
||||||
|
const result = await parseXML(response.data as string);
|
||||||
|
const stat = parseStat(result, filename, isDetailed);
|
||||||
|
return processResponsePayload(response, stat, isDetailed);
|
||||||
|
}
|
@ -0,0 +1,108 @@
|
|||||||
|
import axios from "axios";
|
||||||
|
import { getPatcher } from "./compat/patcher";
|
||||||
|
import { generateDigestAuthHeader, parseDigestAuth } from "./auth/digest";
|
||||||
|
import { cloneShallow, merge } from "./tools/merge";
|
||||||
|
import { mergeHeaders } from "./tools/headers";
|
||||||
|
import {
|
||||||
|
RequestOptionsCustom,
|
||||||
|
RequestOptionsWithState,
|
||||||
|
RequestOptions,
|
||||||
|
Response,
|
||||||
|
WebDAVClientContext,
|
||||||
|
WebDAVMethodOptions
|
||||||
|
} from "./types";
|
||||||
|
|
||||||
|
function _request(requestOptions: RequestOptions) {
|
||||||
|
return getPatcher().patchInline(
|
||||||
|
"request",
|
||||||
|
(options: RequestOptions) => axios(options as any),
|
||||||
|
requestOptions
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function prepareRequestOptions(
|
||||||
|
requestOptions: RequestOptionsCustom | RequestOptionsWithState,
|
||||||
|
context: WebDAVClientContext,
|
||||||
|
userOptions: WebDAVMethodOptions
|
||||||
|
): RequestOptionsWithState {
|
||||||
|
const finalOptions = cloneShallow(requestOptions) as RequestOptionsWithState;
|
||||||
|
finalOptions.headers = mergeHeaders(
|
||||||
|
context.headers,
|
||||||
|
finalOptions.headers || {},
|
||||||
|
userOptions.headers || {}
|
||||||
|
);
|
||||||
|
if (typeof userOptions.data !== "undefined") {
|
||||||
|
finalOptions.data = userOptions.data;
|
||||||
|
}
|
||||||
|
if (context.httpAgent) {
|
||||||
|
finalOptions.httpAgent = context.httpAgent;
|
||||||
|
}
|
||||||
|
if (context.httpsAgent) {
|
||||||
|
finalOptions.httpsAgent = context.httpsAgent;
|
||||||
|
}
|
||||||
|
if (context.digest) {
|
||||||
|
finalOptions._digest = context.digest;
|
||||||
|
}
|
||||||
|
if (typeof context.withCredentials === "boolean") {
|
||||||
|
finalOptions.withCredentials = context.withCredentials;
|
||||||
|
}
|
||||||
|
if (context.maxContentLength) {
|
||||||
|
finalOptions.maxContentLength = context.maxContentLength;
|
||||||
|
}
|
||||||
|
if (context.maxBodyLength) {
|
||||||
|
finalOptions.maxBodyLength = context.maxBodyLength;
|
||||||
|
}
|
||||||
|
if (userOptions.hasOwnProperty("onUploadProgress")) {
|
||||||
|
finalOptions.onUploadProgress = userOptions["onUploadProgress"];
|
||||||
|
}
|
||||||
|
// Take full control of all response status codes
|
||||||
|
finalOptions.validateStatus = () => true;
|
||||||
|
return finalOptions;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function request(requestOptions: RequestOptionsWithState): Promise<Response> {
|
||||||
|
// Client not configured for digest authentication
|
||||||
|
if (!requestOptions._digest) {
|
||||||
|
return _request(requestOptions);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove client's digest authentication object from request options
|
||||||
|
const _digest = requestOptions._digest;
|
||||||
|
delete requestOptions._digest;
|
||||||
|
|
||||||
|
// If client is already using digest authentication, include the digest authorization header
|
||||||
|
if (_digest.hasDigestAuth) {
|
||||||
|
requestOptions = merge(requestOptions, {
|
||||||
|
headers: {
|
||||||
|
Authorization: generateDigestAuthHeader(requestOptions, _digest)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Perform the request and handle digest authentication
|
||||||
|
return _request(requestOptions).then(function(response: Response) {
|
||||||
|
if (response.status == 401) {
|
||||||
|
_digest.hasDigestAuth = parseDigestAuth(response, _digest);
|
||||||
|
|
||||||
|
if (_digest.hasDigestAuth) {
|
||||||
|
requestOptions = merge(requestOptions, {
|
||||||
|
headers: {
|
||||||
|
Authorization: generateDigestAuthHeader(requestOptions, _digest)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return _request(requestOptions).then(function(response2: Response) {
|
||||||
|
if (response2.status == 401) {
|
||||||
|
_digest.hasDigestAuth = false;
|
||||||
|
} else {
|
||||||
|
_digest.nc++;
|
||||||
|
}
|
||||||
|
return response2;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
_digest.nc++;
|
||||||
|
}
|
||||||
|
return response;
|
||||||
|
});
|
||||||
|
}
|
@ -0,0 +1,46 @@
|
|||||||
|
import minimatch from "minimatch";
|
||||||
|
import {
|
||||||
|
FileStat,
|
||||||
|
Response,
|
||||||
|
ResponseDataDetailed,
|
||||||
|
WebDAVClientContext,
|
||||||
|
WebDAVClientError
|
||||||
|
} from "./types";
|
||||||
|
|
||||||
|
export function createErrorFromResponse(response: Response, prefix: string = ""): Error {
|
||||||
|
const err: WebDAVClientError = new Error(
|
||||||
|
`${prefix}Invalid response: ${response.status} ${response.statusText}`
|
||||||
|
) as WebDAVClientError;
|
||||||
|
err.status = response.status;
|
||||||
|
err.response = response;
|
||||||
|
return err;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function handleResponseCode(context: WebDAVClientContext, response: Response): Response {
|
||||||
|
const { status } = response;
|
||||||
|
if (status === 401 && context.digest) return response;
|
||||||
|
if (status >= 400) {
|
||||||
|
const err = createErrorFromResponse(response);
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function processGlobFilter(files: Array<FileStat>, glob: string): Array<FileStat> {
|
||||||
|
return files.filter(file => minimatch(file.filename, glob, { matchBase: true }));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function processResponsePayload<T>(
|
||||||
|
response: Response,
|
||||||
|
data: T,
|
||||||
|
isDetailed: boolean = false
|
||||||
|
): ResponseDataDetailed<T> | T {
|
||||||
|
return isDetailed
|
||||||
|
? {
|
||||||
|
data,
|
||||||
|
headers: response.headers || {},
|
||||||
|
status: response.status,
|
||||||
|
statusText: response.statusText
|
||||||
|
}
|
||||||
|
: data;
|
||||||
|
}
|
@ -0,0 +1,16 @@
|
|||||||
|
import md5 from "md5";
|
||||||
|
|
||||||
|
export function ha1Compute(
|
||||||
|
algorithm: string,
|
||||||
|
user: string,
|
||||||
|
realm: string,
|
||||||
|
pass: string,
|
||||||
|
nonce: string,
|
||||||
|
cnonce: string
|
||||||
|
): string {
|
||||||
|
const ha1 = md5(`${user}:${realm}:${pass}`) as string;
|
||||||
|
if (algorithm && algorithm.toLowerCase() === "md5-sess") {
|
||||||
|
return md5(`${ha1}:${nonce}:${cnonce}`) as string;
|
||||||
|
}
|
||||||
|
return ha1;
|
||||||
|
}
|
@ -0,0 +1,171 @@
|
|||||||
|
import path from "path-posix";
|
||||||
|
import xmlParser from "fast-xml-parser";
|
||||||
|
import nestedProp from "nested-property";
|
||||||
|
import { decodeHTMLEntities } from "./encode";
|
||||||
|
import { normalisePath } from "./path";
|
||||||
|
import {
|
||||||
|
DAVResult,
|
||||||
|
DAVResultRaw,
|
||||||
|
DAVResultResponse,
|
||||||
|
DAVResultResponseProps,
|
||||||
|
DiskQuotaAvailable,
|
||||||
|
FileStat,
|
||||||
|
WebDAVClientError
|
||||||
|
} from "../types";
|
||||||
|
|
||||||
|
enum PropertyType {
|
||||||
|
Array = "array",
|
||||||
|
Object = "object",
|
||||||
|
Original = "original"
|
||||||
|
}
|
||||||
|
|
||||||
|
function getPropertyOfType(
|
||||||
|
obj: Object,
|
||||||
|
prop: string,
|
||||||
|
type: PropertyType = PropertyType.Original
|
||||||
|
): any {
|
||||||
|
const val = nestedProp.get(obj, prop);
|
||||||
|
if (type === "array" && Array.isArray(val) === false) {
|
||||||
|
return [val];
|
||||||
|
} else if (type === "object" && Array.isArray(val)) {
|
||||||
|
return val[0];
|
||||||
|
}
|
||||||
|
return val;
|
||||||
|
}
|
||||||
|
|
||||||
|
function normaliseResponse(response: any): DAVResultResponse {
|
||||||
|
const output = Object.assign({}, response);
|
||||||
|
nestedProp.set(output, "propstat", getPropertyOfType(output, "propstat", PropertyType.Object));
|
||||||
|
nestedProp.set(
|
||||||
|
output,
|
||||||
|
"propstat.prop",
|
||||||
|
getPropertyOfType(output, "propstat.prop", PropertyType.Object)
|
||||||
|
);
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
|
||||||
|
function normaliseResult(result: DAVResultRaw): DAVResult {
|
||||||
|
const { multistatus } = result;
|
||||||
|
if (multistatus === "") {
|
||||||
|
return {
|
||||||
|
multistatus: {
|
||||||
|
response: []
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (!multistatus) {
|
||||||
|
throw new Error("Invalid response: No root multistatus found");
|
||||||
|
}
|
||||||
|
const output: any = {
|
||||||
|
multistatus: Array.isArray(multistatus) ? multistatus[0] : multistatus
|
||||||
|
};
|
||||||
|
nestedProp.set(
|
||||||
|
output,
|
||||||
|
"multistatus.response",
|
||||||
|
getPropertyOfType(output, "multistatus.response", PropertyType.Array)
|
||||||
|
);
|
||||||
|
nestedProp.set(
|
||||||
|
output,
|
||||||
|
"multistatus.response",
|
||||||
|
nestedProp.get(output, "multistatus.response").map(response => normaliseResponse(response))
|
||||||
|
);
|
||||||
|
return output as DAVResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parseXML(xml: string): Promise<DAVResult> {
|
||||||
|
return new Promise(resolve => {
|
||||||
|
const result = xmlParser.parse(xml, {
|
||||||
|
arrayMode: false,
|
||||||
|
ignoreNameSpace: true
|
||||||
|
// // We don't use the processors here as decoding is done manually
|
||||||
|
// // later on - decoding early would break some path checks.
|
||||||
|
// attrValueProcessor: val => decodeHTMLEntities(decodeURIComponent(val)),
|
||||||
|
// tagValueProcessor: val => decodeHTMLEntities(decodeURIComponent(val))
|
||||||
|
});
|
||||||
|
resolve(normaliseResult(result));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function prepareFileFromProps(
|
||||||
|
props: DAVResultResponseProps,
|
||||||
|
rawFilename: string,
|
||||||
|
isDetailed: boolean = false
|
||||||
|
): FileStat {
|
||||||
|
// Last modified time, raw size, item type and mime
|
||||||
|
const {
|
||||||
|
getlastmodified: lastMod = null,
|
||||||
|
getcontentlength: rawSize = "0",
|
||||||
|
resourcetype: resourceType = null,
|
||||||
|
getcontenttype: mimeType = null,
|
||||||
|
getetag: etag = null
|
||||||
|
} = props;
|
||||||
|
const type =
|
||||||
|
resourceType &&
|
||||||
|
typeof resourceType === "object" &&
|
||||||
|
typeof resourceType.collection !== "undefined"
|
||||||
|
? "directory"
|
||||||
|
: "file";
|
||||||
|
const filename = decodeHTMLEntities(rawFilename);
|
||||||
|
const stat: FileStat = {
|
||||||
|
filename,
|
||||||
|
basename: path.basename(filename),
|
||||||
|
lastmod: lastMod,
|
||||||
|
size: parseInt(rawSize, 10),
|
||||||
|
type,
|
||||||
|
etag: typeof etag === "string" ? etag.replace(/"/g, "") : null
|
||||||
|
};
|
||||||
|
if (type === "file") {
|
||||||
|
stat.mime = mimeType && typeof mimeType === "string" ? mimeType.split(";")[0] : "";
|
||||||
|
}
|
||||||
|
if (isDetailed) {
|
||||||
|
stat.props = props;
|
||||||
|
}
|
||||||
|
return stat;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parseStat(
|
||||||
|
result: DAVResult,
|
||||||
|
filename: string,
|
||||||
|
isDetailed: boolean = false
|
||||||
|
): FileStat {
|
||||||
|
let responseItem: DAVResultResponse = null;
|
||||||
|
try {
|
||||||
|
responseItem = result.multistatus.response[0];
|
||||||
|
} catch (e) {
|
||||||
|
/* ignore */
|
||||||
|
}
|
||||||
|
if (!responseItem) {
|
||||||
|
throw new Error("Failed getting item stat: bad response");
|
||||||
|
}
|
||||||
|
const {
|
||||||
|
propstat: { prop: props, status: statusLine }
|
||||||
|
} = responseItem;
|
||||||
|
|
||||||
|
// As defined in https://tools.ietf.org/html/rfc2068#section-6.1
|
||||||
|
const [_, statusCodeStr, statusText] = statusLine.split(" ", 3);
|
||||||
|
const statusCode = parseInt(statusCodeStr, 10);
|
||||||
|
if (statusCode >= 400) {
|
||||||
|
const err: WebDAVClientError = new Error(
|
||||||
|
`Invalid response: ${statusCode} ${statusText}`
|
||||||
|
) as WebDAVClientError;
|
||||||
|
err.status = statusCode;
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
|
||||||
|
const filePath = normalisePath(filename);
|
||||||
|
return prepareFileFromProps(props, filePath, isDetailed);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function translateDiskSpace(value: string | number): DiskQuotaAvailable {
|
||||||
|
switch (value.toString()) {
|
||||||
|
case "-3":
|
||||||
|
return "unlimited";
|
||||||
|
case "-2":
|
||||||
|
/* falls-through */
|
||||||
|
case "-1":
|
||||||
|
// -1 is non-computed
|
||||||
|
return "unknown";
|
||||||
|
default:
|
||||||
|
return parseInt(value as string, 10);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,24 @@
|
|||||||
|
import { decode, encode } from "base-64";
|
||||||
|
|
||||||
|
declare var WEB: boolean;
|
||||||
|
|
||||||
|
export function decodeHTMLEntities(text: string): string {
|
||||||
|
if (typeof WEB === "undefined") {
|
||||||
|
// Node
|
||||||
|
const he = require("he");
|
||||||
|
return he.decode(text);
|
||||||
|
} else {
|
||||||
|
// Nasty browser way
|
||||||
|
const txt = document.createElement("textarea");
|
||||||
|
txt.innerHTML = text;
|
||||||
|
return txt.value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function fromBase64(text: string): string {
|
||||||
|
return decode(text);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function toBase64(text: string): string {
|
||||||
|
return encode(text);
|
||||||
|
}
|
@ -0,0 +1,18 @@
|
|||||||
|
import { Headers } from "../types";
|
||||||
|
|
||||||
|
export function mergeHeaders(...headerPayloads: Headers[]): Headers {
|
||||||
|
if (headerPayloads.length === 0) return {};
|
||||||
|
const headerKeys = {};
|
||||||
|
return headerPayloads.reduce((output: Headers, headers: Headers) => {
|
||||||
|
Object.keys(headers).forEach(header => {
|
||||||
|
const lowerHeader = header.toLowerCase();
|
||||||
|
if (headerKeys.hasOwnProperty(lowerHeader)) {
|
||||||
|
output[headerKeys[lowerHeader]] = headers[header];
|
||||||
|
} else {
|
||||||
|
headerKeys[lowerHeader] = header;
|
||||||
|
output[header] = headers[header];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return output;
|
||||||
|
}, {});
|
||||||
|
}
|
@ -0,0 +1,62 @@
|
|||||||
|
export function cloneShallow<T extends Object>(obj: T): T {
|
||||||
|
return isPlainObject(obj)
|
||||||
|
? Object.assign({}, obj)
|
||||||
|
: Object.setPrototypeOf(Object.assign({}, obj), Object.getPrototypeOf(obj));
|
||||||
|
}
|
||||||
|
|
||||||
|
function isPlainObject(obj: Object | any): boolean {
|
||||||
|
if (
|
||||||
|
typeof obj !== "object" ||
|
||||||
|
obj === null ||
|
||||||
|
Object.prototype.toString.call(obj) != "[object Object]"
|
||||||
|
) {
|
||||||
|
// Not an object
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (Object.getPrototypeOf(obj) === null) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
let proto = obj;
|
||||||
|
// Find the prototype
|
||||||
|
while (Object.getPrototypeOf(proto) !== null) {
|
||||||
|
proto = Object.getPrototypeOf(proto);
|
||||||
|
}
|
||||||
|
return Object.getPrototypeOf(obj) === proto;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function merge(...args: Object[]) {
|
||||||
|
let output = null,
|
||||||
|
items = [...args];
|
||||||
|
while (items.length > 0) {
|
||||||
|
const nextItem = items.shift();
|
||||||
|
if (!output) {
|
||||||
|
output = cloneShallow(nextItem);
|
||||||
|
} else {
|
||||||
|
output = mergeObjects(output, nextItem);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
|
||||||
|
function mergeObjects(obj1: Object, obj2: Object): Object {
|
||||||
|
const output = cloneShallow(obj1);
|
||||||
|
Object.keys(obj2).forEach(key => {
|
||||||
|
if (!output.hasOwnProperty(key)) {
|
||||||
|
output[key] = obj2[key];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (Array.isArray(obj2[key])) {
|
||||||
|
output[key] = Array.isArray(output[key])
|
||||||
|
? [...output[key], ...obj2[key]]
|
||||||
|
: [...obj2[key]];
|
||||||
|
} else if (typeof obj2[key] === "object" && !!obj2[key]) {
|
||||||
|
output[key] =
|
||||||
|
typeof output[key] === "object" && !!output[key]
|
||||||
|
? mergeObjects(output[key], obj2[key])
|
||||||
|
: cloneShallow(obj2[key]);
|
||||||
|
} else {
|
||||||
|
output[key] = obj2[key];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return output;
|
||||||
|
}
|
@ -0,0 +1,36 @@
|
|||||||
|
import { dirname } from "path-posix";
|
||||||
|
|
||||||
|
const SEP_PATH_POSIX = "__PATH_SEPARATOR_POSIX__";
|
||||||
|
const SEP_PATH_WINDOWS = "__PATH_SEPARATOR_WINDOWS__";
|
||||||
|
|
||||||
|
export function encodePath(path) {
|
||||||
|
const replaced = path.replace(/\//g, SEP_PATH_POSIX).replace(/\\\\/g, SEP_PATH_WINDOWS);
|
||||||
|
const formatted = encodeURIComponent(replaced);
|
||||||
|
return formatted
|
||||||
|
.split(SEP_PATH_WINDOWS)
|
||||||
|
.join("\\\\")
|
||||||
|
.split(SEP_PATH_POSIX)
|
||||||
|
.join("/");
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getAllDirectories(path: string): Array<string> {
|
||||||
|
if (!path || path === "/") return [];
|
||||||
|
let currentPath = path;
|
||||||
|
const output: Array<string> = [];
|
||||||
|
do {
|
||||||
|
output.push(currentPath);
|
||||||
|
currentPath = dirname(currentPath);
|
||||||
|
} while (currentPath && currentPath !== "/");
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function normalisePath(pathStr: string): string {
|
||||||
|
let normalisedPath = pathStr;
|
||||||
|
if (normalisedPath[0] !== "/") {
|
||||||
|
normalisedPath = "/" + normalisedPath;
|
||||||
|
}
|
||||||
|
if (/^.+\/$/.test(normalisedPath)) {
|
||||||
|
normalisedPath = normalisedPath.substr(0, normalisedPath.length - 1);
|
||||||
|
}
|
||||||
|
return normalisedPath;
|
||||||
|
}
|
@ -0,0 +1,22 @@
|
|||||||
|
import { translateDiskSpace } from "./dav";
|
||||||
|
import { DAVResult, DiskQuota } from "../types";
|
||||||
|
|
||||||
|
export function parseQuota(result: DAVResult): DiskQuota | null {
|
||||||
|
try {
|
||||||
|
const [responseItem] = result.multistatus.response;
|
||||||
|
const {
|
||||||
|
propstat: {
|
||||||
|
prop: { "quota-used-bytes": quotaUsed, "quota-available-bytes": quotaAvail }
|
||||||
|
}
|
||||||
|
} = responseItem;
|
||||||
|
return typeof quotaUsed !== "undefined" && typeof quotaAvail !== "undefined"
|
||||||
|
? {
|
||||||
|
used: parseInt(quotaUsed, 10),
|
||||||
|
available: translateDiskSpace(quotaAvail)
|
||||||
|
}
|
||||||
|
: null;
|
||||||
|
} catch (err) {
|
||||||
|
/* ignore */
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
@ -0,0 +1,22 @@
|
|||||||
|
import { Layerr } from "layerr";
|
||||||
|
import { isArrayBuffer } from "../compat/arrayBuffer";
|
||||||
|
import { isBuffer } from "../compat/buffer";
|
||||||
|
import { BufferLike, ErrorCode } from "../types";
|
||||||
|
|
||||||
|
export function calculateDataLength(data: string | BufferLike): number {
|
||||||
|
if (isArrayBuffer(data)) {
|
||||||
|
return (<ArrayBuffer>data).byteLength;
|
||||||
|
} else if (isBuffer(data)) {
|
||||||
|
return (<Buffer>data).length;
|
||||||
|
} else if (typeof data === "string") {
|
||||||
|
return (<string>data).length;
|
||||||
|
}
|
||||||
|
throw new Layerr(
|
||||||
|
{
|
||||||
|
info: {
|
||||||
|
code: ErrorCode.DataTypeNoLength
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Cannot calculate data length: Invalid type"
|
||||||
|
);
|
||||||
|
}
|
@ -0,0 +1,32 @@
|
|||||||
|
import URL from "url-parse";
|
||||||
|
import _joinURL from "url-join";
|
||||||
|
import { normalisePath } from "./path";
|
||||||
|
|
||||||
|
export function extractURLPath(fullURL: string): string {
|
||||||
|
const url = new URL(fullURL);
|
||||||
|
let urlPath = url.pathname;
|
||||||
|
if (urlPath.length <= 0) {
|
||||||
|
urlPath = "/";
|
||||||
|
}
|
||||||
|
return normalisePath(urlPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function joinURL(...parts: Array<string>): string {
|
||||||
|
return _joinURL(
|
||||||
|
parts.reduce((output, nextPart, partIndex) => {
|
||||||
|
if (
|
||||||
|
partIndex === 0 ||
|
||||||
|
nextPart !== "/" ||
|
||||||
|
(nextPart === "/" && output[output.length - 1] !== "/")
|
||||||
|
) {
|
||||||
|
output.push(nextPart);
|
||||||
|
}
|
||||||
|
return output;
|
||||||
|
}, [])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function normaliseHREF(href: string): string {
|
||||||
|
const normalisedHref = href.replace(/^https?:\/\/[^\/]+/, "");
|
||||||
|
return normalisedHref;
|
||||||
|
}
|
@ -0,0 +1,55 @@
|
|||||||
|
import xmlParser, { j2xParser as XMLParser } from "fast-xml-parser";
|
||||||
|
|
||||||
|
export function generateLockXML(ownerHREF: string): string {
|
||||||
|
return getParser().parse(
|
||||||
|
namespace(
|
||||||
|
{
|
||||||
|
lockinfo: {
|
||||||
|
"@_xmlns:d": "DAV:",
|
||||||
|
lockscope: {
|
||||||
|
exclusive: {}
|
||||||
|
},
|
||||||
|
locktype: {
|
||||||
|
write: {}
|
||||||
|
},
|
||||||
|
owner: {
|
||||||
|
href: ownerHREF
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"d"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getParser(): XMLParser {
|
||||||
|
return new XMLParser({
|
||||||
|
attributeNamePrefix: "@_",
|
||||||
|
format: true,
|
||||||
|
ignoreAttributes: false,
|
||||||
|
supressEmptyNode: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function namespace<T extends Object>(obj: T, ns: string): T {
|
||||||
|
const copy = { ...obj };
|
||||||
|
for (const key in copy) {
|
||||||
|
if (copy[key] && typeof copy[key] === "object" && key.indexOf(":") === -1) {
|
||||||
|
copy[`${ns}:${key}`] = namespace(copy[key], ns);
|
||||||
|
delete copy[key];
|
||||||
|
} else if (/^@_/.test(key) === false) {
|
||||||
|
copy[`${ns}:${key}`] = copy[key];
|
||||||
|
delete copy[key];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return copy;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parseGenericResponse(xml: string): Object {
|
||||||
|
return xmlParser.parse(xml, {
|
||||||
|
arrayMode: false,
|
||||||
|
ignoreNameSpace: true,
|
||||||
|
parseAttributeValue: true,
|
||||||
|
parseNodeValue: true
|
||||||
|
});
|
||||||
|
}
|
@ -0,0 +1,288 @@
|
|||||||
|
import Stream from "stream";
|
||||||
|
|
||||||
|
export type AuthHeader = string;
|
||||||
|
|
||||||
|
export enum AuthType {
|
||||||
|
Digest = "digest",
|
||||||
|
None = "none",
|
||||||
|
Password = "password",
|
||||||
|
Token = "token"
|
||||||
|
}
|
||||||
|
|
||||||
|
export type BufferLike = Buffer | ArrayBuffer;
|
||||||
|
|
||||||
|
export interface CreateDirectoryOptions extends WebDAVMethodOptions {
|
||||||
|
recursive?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CreateReadStreamOptions extends WebDAVMethodOptions {
|
||||||
|
callback?: (response: Response) => void;
|
||||||
|
range?: {
|
||||||
|
start: number;
|
||||||
|
end?: number;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export type CreateWriteStreamCallback = (response: Response) => void;
|
||||||
|
|
||||||
|
export interface CreateWriteStreamOptions extends WebDAVMethodOptions {
|
||||||
|
overwrite?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DAVResultResponse {
|
||||||
|
href: string;
|
||||||
|
propstat: {
|
||||||
|
prop: DAVResultResponseProps;
|
||||||
|
status: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DAVResultResponseProps {
|
||||||
|
displayname: string;
|
||||||
|
resourcetype: {
|
||||||
|
collection?: boolean;
|
||||||
|
};
|
||||||
|
getlastmodified?: string;
|
||||||
|
getetag?: string;
|
||||||
|
getcontentlength?: string;
|
||||||
|
getcontenttype?: string;
|
||||||
|
"quota-available-bytes"?: any;
|
||||||
|
"quota-used-bytes"?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DAVResult {
|
||||||
|
multistatus: {
|
||||||
|
response: Array<DAVResultResponse>;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DAVResultRawMultistatus {
|
||||||
|
response: DAVResultResponse | [DAVResultResponse];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DAVResultRaw {
|
||||||
|
multistatus: "" | DAVResultRawMultistatus | [DAVResultRawMultistatus];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DigestContext {
|
||||||
|
username: string;
|
||||||
|
password: string;
|
||||||
|
nc: number;
|
||||||
|
algorithm: string;
|
||||||
|
hasDigestAuth: boolean;
|
||||||
|
cnonce?: string;
|
||||||
|
nonce?: string;
|
||||||
|
realm?: string;
|
||||||
|
qop?: string;
|
||||||
|
opaque?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DiskQuota {
|
||||||
|
used: number;
|
||||||
|
available: DiskQuotaAvailable;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type DiskQuotaAvailable = "unknown" | "unlimited" | number;
|
||||||
|
|
||||||
|
export enum ErrorCode {
|
||||||
|
DataTypeNoLength = "data-type-no-length",
|
||||||
|
InvalidAuthType = "invalid-auth-type",
|
||||||
|
InvalidOutputFormat = "invalid-output-format",
|
||||||
|
LinkUnsupportedAuthType = "link-unsupported-auth"
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FileStat {
|
||||||
|
filename: string;
|
||||||
|
basename: string;
|
||||||
|
lastmod: string;
|
||||||
|
size: number;
|
||||||
|
type: "file" | "directory";
|
||||||
|
etag: string | null;
|
||||||
|
mime?: string;
|
||||||
|
props?: DAVResultResponseProps;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GetDirectoryContentsOptions extends WebDAVMethodOptions {
|
||||||
|
deep?: boolean;
|
||||||
|
details?: boolean;
|
||||||
|
glob?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GetFileContentsOptions extends WebDAVMethodOptions {
|
||||||
|
details?: boolean;
|
||||||
|
format?: "binary" | "text";
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GetQuotaOptions extends WebDAVMethodOptions {
|
||||||
|
details?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Headers {
|
||||||
|
[key: string]: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LockOptions extends WebDAVMethodOptions {
|
||||||
|
refreshToken?: string;
|
||||||
|
timeout?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LockResponse {
|
||||||
|
serverTimeout: string;
|
||||||
|
token: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface OAuthToken {
|
||||||
|
access_token: string;
|
||||||
|
token_type: string;
|
||||||
|
refresh_token?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PutFileContentsOptions extends WebDAVMethodOptions {
|
||||||
|
contentLength?: boolean | number;
|
||||||
|
overwrite?: boolean;
|
||||||
|
onUploadProgress?: UploadProgressCallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type RequestDataPayload = string | Buffer | ArrayBuffer | { [key: string]: any };
|
||||||
|
|
||||||
|
interface RequestOptionsBase {
|
||||||
|
data?: RequestDataPayload;
|
||||||
|
headers?: Headers;
|
||||||
|
httpAgent?: any;
|
||||||
|
httpsAgent?: any;
|
||||||
|
maxBodyLength?: number;
|
||||||
|
maxContentLength?: number;
|
||||||
|
maxRedirects?: number;
|
||||||
|
method: string;
|
||||||
|
onUploadProgress?: UploadProgressCallback;
|
||||||
|
responseType?: string;
|
||||||
|
transformResponse?: Array<(value: any) => any>;
|
||||||
|
url?: string;
|
||||||
|
validateStatus?: (status: number) => boolean;
|
||||||
|
withCredentials?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RequestOptionsCustom extends RequestOptionsBase {}
|
||||||
|
|
||||||
|
export interface RequestOptions extends RequestOptionsBase {
|
||||||
|
url: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RequestOptionsWithState extends RequestOptions {
|
||||||
|
_digest?: DigestContext;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Response {
|
||||||
|
data: ResponseData;
|
||||||
|
status: number;
|
||||||
|
headers: Headers;
|
||||||
|
statusText: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ResponseData = string | Buffer | ArrayBuffer | Object | Array<any>;
|
||||||
|
|
||||||
|
export interface ResponseDataDetailed<T> {
|
||||||
|
data: T;
|
||||||
|
headers: Headers;
|
||||||
|
status: number;
|
||||||
|
statusText: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ResponseStatusValidator {
|
||||||
|
(status: number): boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface StatOptions extends WebDAVMethodOptions {
|
||||||
|
details?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UploadProgress {
|
||||||
|
loaded: number;
|
||||||
|
total: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UploadProgressCallback {
|
||||||
|
(progress: UploadProgress): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface WebDAVClient {
|
||||||
|
copyFile: (filename: string, destination: string) => Promise<void>;
|
||||||
|
createDirectory: (path: string, options?: CreateDirectoryOptions) => Promise<void>;
|
||||||
|
createReadStream: (filename: string, options?: CreateReadStreamOptions) => Stream.Readable;
|
||||||
|
createWriteStream: (
|
||||||
|
filename: string,
|
||||||
|
options?: CreateWriteStreamOptions,
|
||||||
|
callback?: CreateWriteStreamCallback
|
||||||
|
) => Stream.Writable;
|
||||||
|
customRequest: (path: string, requestOptions: RequestOptionsCustom) => Promise<Response>;
|
||||||
|
deleteFile: (filename: string) => Promise<void>;
|
||||||
|
exists: (path: string) => Promise<boolean>;
|
||||||
|
getDirectoryContents: (
|
||||||
|
path: string,
|
||||||
|
options?: GetDirectoryContentsOptions
|
||||||
|
) => Promise<Array<FileStat> | ResponseDataDetailed<Array<FileStat>>>;
|
||||||
|
getFileContents: (
|
||||||
|
filename: string,
|
||||||
|
options?: GetFileContentsOptions
|
||||||
|
) => Promise<BufferLike | string | ResponseDataDetailed<BufferLike | string>>;
|
||||||
|
getFileDownloadLink: (filename: string) => string;
|
||||||
|
getFileUploadLink: (filename: string) => string;
|
||||||
|
getHeaders: () => Headers;
|
||||||
|
getQuota: (
|
||||||
|
options?: GetQuotaOptions
|
||||||
|
) => Promise<DiskQuota | null | ResponseDataDetailed<DiskQuota | null>>;
|
||||||
|
lock: (path: string, options?: LockOptions) => Promise<LockResponse>;
|
||||||
|
moveFile: (filename: string, destinationFilename: string) => Promise<void>;
|
||||||
|
putFileContents: (
|
||||||
|
filename: string,
|
||||||
|
data: string | BufferLike | Stream.Readable,
|
||||||
|
options?: PutFileContentsOptions
|
||||||
|
) => Promise<boolean>;
|
||||||
|
setHeaders: (headers: Headers) => void;
|
||||||
|
stat: (
|
||||||
|
path: string,
|
||||||
|
options?: StatOptions
|
||||||
|
) => Promise<FileStat | ResponseDataDetailed<FileStat>>;
|
||||||
|
unlock: (path: string, token: string, options?: WebDAVMethodOptions) => Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface WebDAVClientContext {
|
||||||
|
authType: AuthType;
|
||||||
|
contactHref: string;
|
||||||
|
digest?: DigestContext;
|
||||||
|
headers: Headers;
|
||||||
|
httpAgent?: any;
|
||||||
|
httpsAgent?: any;
|
||||||
|
maxBodyLength?: number;
|
||||||
|
maxContentLength?: number;
|
||||||
|
password?: string;
|
||||||
|
remotePath: string;
|
||||||
|
remoteURL: string;
|
||||||
|
token?: OAuthToken;
|
||||||
|
username?: string;
|
||||||
|
withCredentials?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface WebDAVClientError extends Error {
|
||||||
|
status?: number;
|
||||||
|
response?: Response;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface WebDAVClientOptions {
|
||||||
|
authType?: AuthType;
|
||||||
|
contactHref?: string;
|
||||||
|
headers?: Headers;
|
||||||
|
httpAgent?: any;
|
||||||
|
httpsAgent?: any;
|
||||||
|
maxBodyLength?: number;
|
||||||
|
maxContentLength?: number;
|
||||||
|
password?: string;
|
||||||
|
token?: OAuthToken;
|
||||||
|
username?: string;
|
||||||
|
withCredentials?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface WebDAVMethodOptions {
|
||||||
|
data?: RequestDataPayload;
|
||||||
|
headers?: Headers;
|
||||||
|
}
|
@ -0,0 +1,121 @@
|
|||||||
|
<template>
|
||||||
|
<BaseFrame v-model="shown" :is-dark="isDark">
|
||||||
|
<template #title>
|
||||||
|
{{ self.Name }}
|
||||||
|
</template>
|
||||||
|
<div class="flex justify-center items-center">
|
||||||
|
<n-avatar style="--color: none" :src="Cfg.host.value + usr.Icon"
|
||||||
|
round></n-avatar>
|
||||||
|
</div>
|
||||||
|
<template #main>
|
||||||
|
<div style="height: 100%">
|
||||||
|
<div style="height: calc(100% - 50px)">
|
||||||
|
<div class="w-full px-3">
|
||||||
|
<div class="h-16 flex justify-between items-center">
|
||||||
|
<span style="color: #777">我的账户</span>
|
||||||
|
<span @click="$router.push({name: 'user_setting'});shown=false" class="cursor-pointer"
|
||||||
|
style="color:#f36828">账户中心</span>
|
||||||
|
</div>
|
||||||
|
<div class="grid grid-cols-4 gap-4 h-20">
|
||||||
|
<div class="flex items-center justify-center">
|
||||||
|
<n-avatar size="50" :src="Cfg.host.value+ usr.Icon" round></n-avatar>
|
||||||
|
</div>
|
||||||
|
<div class="col-span-2 text-xs grid grid-cols-1 items-center text-left" style="">
|
||||||
|
<span>昵称:    {{ usr.Nickname }}</span>
|
||||||
|
<span>账户:    {{ usr.Username }}</span>
|
||||||
|
<span>邮箱:    {{ usr.Email }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="">123</div>
|
||||||
|
</div>
|
||||||
|
<hr class="mt-10" style="border:none;border-top:1px solid #777;">
|
||||||
|
</div>
|
||||||
|
<File :usr="usr"></File>
|
||||||
|
<Apps :apps="ofApps"></Apps>
|
||||||
|
</div>
|
||||||
|
<hr style="border:none;border-top:2px solid #777;">
|
||||||
|
<div style="height: 48px">
|
||||||
|
<div @click="evt.emit('logout')"
|
||||||
|
class="w-full h-full flex justify-center items-center cursor-pointer transition duration-500 ease-in-out transform hover:scale-125">
|
||||||
|
<OneIcon :color="isDark?'#eee': '#333'" class="inline-block" style="font-size: 24px;">
|
||||||
|
logout
|
||||||
|
</OneIcon>
|
||||||
|
<div>
|
||||||
|
退出登录
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</BaseFrame>
|
||||||
|
</template>
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import BaseFrame from './frame.vue'
|
||||||
|
import Apps from './components/app.vue'
|
||||||
|
import File from './components/file.vue'
|
||||||
|
import {OneIcon} from '@veypi/one-icon'
|
||||||
|
import {computed, onMounted, ref, watch} from 'vue'
|
||||||
|
import {decode} from 'js-base64'
|
||||||
|
import {api, Cfg} from './api'
|
||||||
|
import evt from './evt'
|
||||||
|
import {modelsApp, modelsUser} from './models'
|
||||||
|
|
||||||
|
let shown = ref(false)
|
||||||
|
let emits = defineEmits<{
|
||||||
|
(e: 'logout'): void
|
||||||
|
(e: 'load', u: modelsUser): void
|
||||||
|
}>()
|
||||||
|
let props = withDefaults(defineProps<{
|
||||||
|
isDark?: boolean
|
||||||
|
}>(), {
|
||||||
|
isDark: false,
|
||||||
|
})
|
||||||
|
onMounted(() => {
|
||||||
|
fetchUserData()
|
||||||
|
})
|
||||||
|
|
||||||
|
let usr = ref<modelsUser>({} as modelsUser)
|
||||||
|
let ofApps = ref<modelsApp[]>([])
|
||||||
|
let self = ref<modelsApp>({} as modelsApp)
|
||||||
|
|
||||||
|
let token = computed(() => Cfg.token.value)
|
||||||
|
watch(token, () => {
|
||||||
|
fetchUserData()
|
||||||
|
})
|
||||||
|
|
||||||
|
function fetchUserData() {
|
||||||
|
let token = Cfg.token.value?.split('.')
|
||||||
|
if (!token || token.length !== 3) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
let data = JSON.parse(decode(token[1]))
|
||||||
|
if (data.ID > 0) {
|
||||||
|
api.user.get(data.ID).Start(e => {
|
||||||
|
usr.value = e
|
||||||
|
ofApps.value = []
|
||||||
|
for (let v of e.Apps) {
|
||||||
|
if (v.Status === 'ok') {
|
||||||
|
ofApps.value.push(v.App)
|
||||||
|
}
|
||||||
|
if (v.App.UUID === Cfg.uuid.value) {
|
||||||
|
self.value = v.App
|
||||||
|
}
|
||||||
|
}
|
||||||
|
emits('load', e)
|
||||||
|
}, e => {
|
||||||
|
console.log(e)
|
||||||
|
evt.emit('logout')
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
evt.emit('logout')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
evt.on('logout', () => {
|
||||||
|
emits('logout')
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
@ -0,0 +1,72 @@
|
|||||||
|
import {darkTheme} from 'naive-ui/lib/themes'
|
||||||
|
import {BuiltInGlobalTheme} from 'naive-ui/lib/themes/interface'
|
||||||
|
import {lightTheme} from 'naive-ui/lib/themes/light'
|
||||||
|
import {ref} from 'vue'
|
||||||
|
import {useOsTheme, GlobalThemeOverrides} from 'naive-ui'
|
||||||
|
|
||||||
|
interface builtIn extends BuiltInGlobalTheme {
|
||||||
|
overrides: GlobalThemeOverrides
|
||||||
|
me: {
|
||||||
|
lightBox: string,
|
||||||
|
lightBoxShadow: string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let light = lightTheme as builtIn
|
||||||
|
let dark = darkTheme as builtIn
|
||||||
|
let intputNone = {
|
||||||
|
color: 'url(0) no-repeat',
|
||||||
|
colorFocus: 'url(0) no-repeat',
|
||||||
|
colorFocusWarning: 'url(0) no-repeat',
|
||||||
|
colorFocusError: 'url(0) no-repeat',
|
||||||
|
}
|
||||||
|
light.overrides = {
|
||||||
|
Input: Object.assign({}, intputNone),
|
||||||
|
}
|
||||||
|
dark.overrides = {
|
||||||
|
Input: Object.assign({
|
||||||
|
border: '1px solid #aaa',
|
||||||
|
}, intputNone),
|
||||||
|
}
|
||||||
|
light.common.cardColor = '#f4f4f4'
|
||||||
|
light.common.bodyColor = '#eee'
|
||||||
|
dark.common.bodyColor = '#2e2e2e'
|
||||||
|
light.me = {
|
||||||
|
lightBox: '#f4f4f4',
|
||||||
|
lightBoxShadow: '18px 18px 36px #c6c6c6, -18px -18px 36px #fff',
|
||||||
|
}
|
||||||
|
|
||||||
|
dark.me = {
|
||||||
|
lightBox: '#2e2e2e',
|
||||||
|
lightBoxShadow: '21px 21px 42px #272727, -21px -21px 42px #353535',
|
||||||
|
}
|
||||||
|
export const OsThemeRef = useOsTheme()
|
||||||
|
|
||||||
|
let theme = 'light'
|
||||||
|
|
||||||
|
export let Theme = ref(light)
|
||||||
|
|
||||||
|
export let IsDark = ref(false)
|
||||||
|
|
||||||
|
function change(t: string) {
|
||||||
|
if (t === 'dark') {
|
||||||
|
theme = 'dark'
|
||||||
|
Theme.value = dark
|
||||||
|
} else {
|
||||||
|
theme = 'light'
|
||||||
|
Theme.value = light
|
||||||
|
}
|
||||||
|
IsDark.value = theme === 'dark'
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ChangeTheme() {
|
||||||
|
if (IsDark.value) {
|
||||||
|
change('light')
|
||||||
|
} else {
|
||||||
|
change('dark')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (OsThemeRef.value === 'dark') {
|
||||||
|
change('dark')
|
||||||
|
}
|
@ -0,0 +1,11 @@
|
|||||||
|
module.exports = {
|
||||||
|
purge: ['./index.html', './src/**/*.{vue,js,ts,jsx,tsx}'],
|
||||||
|
darkMode: false, // or 'media' or 'class'
|
||||||
|
theme: {
|
||||||
|
extend: {},
|
||||||
|
},
|
||||||
|
variants: {
|
||||||
|
extend: {},
|
||||||
|
},
|
||||||
|
plugins: [],
|
||||||
|
}
|
@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "esnext",
|
||||||
|
"useDefineForClassFields": true,
|
||||||
|
"module": "esnext",
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"strict": true,
|
||||||
|
"jsx": "preserve",
|
||||||
|
"sourceMap": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"lib": ["esnext", "dom"]
|
||||||
|
},
|
||||||
|
"include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"]
|
||||||
|
}
|
@ -0,0 +1,7 @@
|
|||||||
|
import { defineConfig } from 'vite'
|
||||||
|
import vue from '@vitejs/plugin-vue'
|
||||||
|
|
||||||
|
// https://vitejs.dev/config/
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [vue()]
|
||||||
|
})
|
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,37 @@
|
|||||||
|
<template>
|
||||||
|
<div class="flex justify-center items-center w-full h-full">
|
||||||
|
<div class="text-center text-xl">
|
||||||
|
<q-icon style="font-size: 200px" name="svguse:#icon-404"></q-icon>
|
||||||
|
<div>
|
||||||
|
路径失效啦! {{ count }}秒
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { useRouter, useRoute } from 'vue-router'
|
||||||
|
import { onBeforeUnmount, onMounted, ref } from "vue";
|
||||||
|
|
||||||
|
const route = useRoute()
|
||||||
|
const router = useRouter()
|
||||||
|
let count = ref(5)
|
||||||
|
let timer = ref()
|
||||||
|
onMounted(() => {
|
||||||
|
timer.value = setInterval(() => {
|
||||||
|
count.value--
|
||||||
|
if (count.value === 0) {
|
||||||
|
router.back()
|
||||||
|
// router.push('/')
|
||||||
|
clearInterval(timer.value)
|
||||||
|
}
|
||||||
|
}, 1000)
|
||||||
|
})
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
if (timer.value) {
|
||||||
|
clearInterval(timer.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
<style scoped></style>
|
@ -0,0 +1,23 @@
|
|||||||
|
<!--
|
||||||
|
* AppHome.vue
|
||||||
|
* Copyright (C) 2023 veypi <i@veypi.com>
|
||||||
|
* 2023-10-04 15:34
|
||||||
|
* Distributed under terms of the MIT license.
|
||||||
|
-->
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
{{ app }}
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { inject } from 'vue';
|
||||||
|
import { modelsApp } from 'src/models';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
let app = inject('app') as modelsApp
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped></style>
|
||||||
|
|
@ -0,0 +1,47 @@
|
|||||||
|
/*
|
||||||
|
* menu.ts
|
||||||
|
* Copyright (C) 2023 veypi <i@veypi.com>
|
||||||
|
* 2023-10-04 21:51
|
||||||
|
* Distributed under terms of the MIT license.
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
import { defineStore } from 'pinia';
|
||||||
|
import { MenuLink } from 'src/models';
|
||||||
|
|
||||||
|
const defaultLinks: MenuLink[] = [
|
||||||
|
{
|
||||||
|
title: '应用中心',
|
||||||
|
caption: '',
|
||||||
|
icon: 'apps',
|
||||||
|
to: { name: 'home' }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '用户管理',
|
||||||
|
caption: 'oa.veypi.com',
|
||||||
|
icon: 'person',
|
||||||
|
to: { name: 'user' }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '设置',
|
||||||
|
caption: '',
|
||||||
|
icon: 'settings',
|
||||||
|
to: { name: 'settings' }
|
||||||
|
},
|
||||||
|
]
|
||||||
|
export const useMenuStore = defineStore('menu', {
|
||||||
|
state: () => ({
|
||||||
|
list: defaultLinks as MenuLink[],
|
||||||
|
}),
|
||||||
|
getters: {
|
||||||
|
},
|
||||||
|
actions: {
|
||||||
|
set(links: MenuLink[]) {
|
||||||
|
this.list = links
|
||||||
|
|
||||||
|
},
|
||||||
|
load_default() {
|
||||||
|
this.list = defaultLinks
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
Loading…
Reference in New Issue