remove oaer and fix cors permissive

master
veypi 1 year ago
parent 6d17b8cb35
commit 5ae9c6eb50

@ -0,0 +1,70 @@
//
// cors.rs
// Copyright (C) 2023 veypi <i@veypi.com>
// 2023-10-06 15:02
// Distributed under terms of the MIT license.
//
use std::future::{ready, Ready};
use actix_web::{
dev::{forward_ready, Service, ServiceRequest, ServiceResponse, Transform},
Error,
};
use futures_util::future::LocalBoxFuture;
// There are two steps in middleware processing.
// 1. Middleware initialization, middleware factory gets called with
// next service in chain as parameter.
// 2. Middleware's call method gets called with normal request.
pub struct cors;
// Middleware factory is `Transform` trait
// `S` - type of the next service
// `B` - type of response's body
impl<S, B> Transform<S, ServiceRequest> for cors
where
S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error>,
S::Future: 'static,
B: 'static,
{
type Response = ServiceResponse<B>;
type Error = Error;
type InitError = ();
type Transform = corsMiddleware<S>;
type Future = Ready<Result<Self::Transform, Self::InitError>>;
fn new_transform(&self, service: S) -> Self::Future {
ready(Ok(corsMiddleware { service }))
}
}
pub struct corsMiddleware<S> {
service: S,
}
impl<S, B> Service<ServiceRequest> for corsMiddleware<S>
where
S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error>,
S::Future: 'static,
B: 'static,
{
type Response = ServiceResponse<B>;
type Error = Error;
type Future = LocalBoxFuture<'static, Result<Self::Response, Self::Error>>;
forward_ready!(service);
fn call(&self, req: ServiceRequest) -> Self::Future {
println!("Hi from start. You requested: {}", req.path());
let fut = self.service.call(req);
Box::pin(async move {
let res = fut.await?;
println!("Hi from response");
Ok(res)
})
}
}

@ -18,7 +18,7 @@ use dav_server::{
DavConfig, DavHandler,
};
use http::Response;
use http::{header, Method, Response};
use http_auth_basic::Credentials;
use sea_orm::{ColumnTrait, EntityTrait, QueryFilter};
use tracing::{info, warn};
@ -34,6 +34,32 @@ pub fn core() -> DavHandler {
.strip_prefix("/file/")
.build_handler()
}
/// Try to parse header value as HTTP method.
fn header_value_try_into_method(hdr: &header::HeaderValue) -> Option<Method> {
hdr.to_str()
.ok()
.and_then(|meth| Method::try_from(meth).ok())
}
fn is_request_preflight(req: &DavRequest) -> bool {
// check request method is OPTIONS
if req.request.method() != Method::OPTIONS {
return false;
}
// check follow-up request method is present and valid
if req
.request
.headers()
.get(header::ACCESS_CONTROL_REQUEST_METHOD)
.and_then(header_value_try_into_method)
.is_none()
{
return false;
}
true
}
pub async fn dav_handler(
req: DavRequest,
@ -41,8 +67,8 @@ pub async fn dav_handler(
stat: web::Data<AppState>,
) -> DavResponse {
let root = stat.fs_root.clone();
match handle_file(req, stat).await {
Ok((p, req)) => {
match handle_file(&req, stat).await {
Ok(p) => {
let p = Path::new(&root).join(p);
if !p.exists() {
match fs::create_dir_all(p.clone()) {
@ -58,6 +84,29 @@ pub async fn dav_handler(
}
Err(e) => {
warn!("handle file failed: {}", e);
if is_request_preflight(&req) {
let origin = match req.request.headers().get("Origin") {
Some(o) => o.to_str().unwrap().clone(),
None => "",
};
Response::builder()
.status(200)
.header("WWW-Authenticate", "Basic realm=\"file\"")
.header("Access-Control-Allow-Origin", origin)
.header("Access-Control-Allow-Credentials", "true")
.header("Access-Control-Allow-Headers", "auth_token, depth")
.header(
"Access-Control-Allow-Methods",
"OPTIONS, DELETE, GET, POST, PUT, HEAD, TRACE, PATCH, CONNECT, PROPFIND",
)
.header(
"Access-Control-Expose-Headers",
"access-control-allow-origin, content-type",
)
.body(Body::from("please auth".to_string()))
.unwrap()
.into()
} else {
Response::builder()
.status(401)
.header("WWW-Authenticate", "Basic realm=\"file\"")
@ -66,9 +115,10 @@ pub async fn dav_handler(
.into()
}
}
}
}
async fn handle_file(req: DavRequest, stat: web::Data<AppState>) -> Result<(String, DavRequest)> {
async fn handle_file(req: &DavRequest, stat: web::Data<AppState>) -> Result<String> {
let p = req.request.uri();
let headers = req.request.headers();
let m = req.request.method();
@ -87,10 +137,10 @@ async fn handle_file(req: DavRequest, stat: web::Data<AppState>) -> Result<(Stri
if app_id != "" {
// 只有秘钥才能访问app数据
if t.can_read("app", app_id) {
return Ok((format!("app/{}/", app_id), req));
return Ok(format!("app/{}/", app_id));
}
} else {
return Ok((format!("user/{}/", t.id), req));
return Ok(format!("user/{}/", t.id));
}
}
}
@ -110,7 +160,7 @@ async fn handle_file(req: DavRequest, stat: web::Data<AppState>) -> Result<(Stri
{
Some(u) => {
u.check_pass(&credentials.password)?;
return Ok((format!("user/{}/", u.id), req));
return Ok(format!("user/{}/", u.id));
}
None => {}
}

@ -6,68 +6,6 @@
//
pub mod auth;
pub mod user;
pub mod cors;
pub mod fs;
use std::future::{ready, Ready};
use actix_web::{
dev::{forward_ready, Service, ServiceRequest, ServiceResponse, Transform},
Error,
};
use futures_util::future::LocalBoxFuture;
// There are two steps in middleware processing.
// 1. Middleware initialization, middleware factory gets called with
// next service in chain as parameter.
// 2. Middleware's call method gets called with normal request.
pub struct SayHi;
// Middleware factory is `Transform` trait
// `S` - type of the next service
// `B` - type of response's body
impl<S, B> Transform<S, ServiceRequest> for SayHi
where
S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error>,
S::Future: 'static,
B: 'static,
{
type Response = ServiceResponse<B>;
type Error = Error;
type InitError = ();
type Transform = SayHiMiddleware<S>;
type Future = Ready<Result<Self::Transform, Self::InitError>>;
fn new_transform(&self, service: S) -> Self::Future {
ready(Ok(SayHiMiddleware { service }))
}
}
pub struct SayHiMiddleware<S> {
service: S,
}
impl<S, B> Service<ServiceRequest> for SayHiMiddleware<S>
where
S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error>,
S::Future: 'static,
B: 'static,
{
type Response = ServiceResponse<B>;
type Error = Error;
type Future = LocalBoxFuture<'static, Result<Self::Response, Self::Error>>;
forward_ready!(service);
fn call(&self, req: ServiceRequest) -> Self::Future {
println!("Hi from start. You requested: {}", req.path());
let fut = self.service.call(req);
Box::pin(async move {
let res = fut.await?;
println!("Hi from response");
Ok(res)
})
}
}
pub mod user;

@ -1,3 +1,4 @@
use actix_cors::Cors;
//
// main.rs
// Copyright (C) 2022 veypi <i@veypi.com>
@ -6,13 +7,15 @@
//
use actix_files as fs;
use actix_web::{
dev,
dev::{self, Service},
http::StatusCode,
middleware::{self, ErrorHandlerResponse, ErrorHandlers},
web::{self},
App, HttpServer,
};
use futures_util::future::FutureExt;
use http::{HeaderName, HeaderValue};
use oab::{api, init_log, libs, models, AppState, Clis, Result, CLI};
use tracing::{error, info, warn};
@ -53,18 +56,15 @@ async fn web(data: AppState) -> Result<()> {
)
.into()
});
let cors = actix_cors::Cors::default()
.allow_any_header()
.allow_any_origin()
.allow_any_method();
let cors = actix_cors::Cors::permissive();
let app = App::new();
app.wrap(logger)
.wrap(cors)
.wrap(middleware::Compress::default())
.app_data(web::Data::new(data.clone()))
.service(fs::Files::new("/media", data.media_path.clone()).show_files_listing())
.service(
web::scope("api")
.wrap(cors)
.wrap(
ErrorHandlers::new()
.handler(StatusCode::INTERNAL_SERVER_ERROR, add_error_header),
@ -75,6 +75,33 @@ async fn web(data: AppState) -> Result<()> {
)
.service(
web::scope("file")
.wrap_fn(|req, srv| {
let headers = &req.headers().clone();
let origin = match headers.get("Origin") {
Some(o) => o.to_str().unwrap().clone().to_string(),
None => "".to_string(),
};
srv.call(req).map(move |res| {
let res = match res {
Ok(mut expr) => {
let headers = expr.headers_mut();
headers.insert(
HeaderName::try_from("Access-Control-Allow-Origin")
.unwrap(),
HeaderValue::from_str(&origin).unwrap(),
);
headers.insert(
HeaderName::try_from("123").unwrap(),
HeaderValue::from_str("asd").unwrap(),
);
Ok(expr)
}
Err(e) => Err(e),
};
println!("Hi from response");
res
})
})
.app_data(web::Data::new(dav.clone()))
.service(web::resource("/{tail:.*}").to(libs::fs::dav_handler)),
)

@ -86,6 +86,9 @@ module.exports = configure(function(/* ctx */) {
extendViteConf(viteConf) {
viteConf.resolve.preserveSymlinks = true;
viteConf.optimizeDeps.exclude = [
'@veypi/oaer'
];
},
// viteVuePluginOptions: {},

@ -10,9 +10,9 @@
import { boot } from 'quasar/wrappers'
import '@veypi/msg/index.css'
import '../assets/icon.js'
// import { Cfg } from '@veypi/oaer'
import '@veypi/oaer/dist/index.css'
import { Cfg } from '@veypi/oaer'
// import { Cfg } from '/Users/veypi/test/oaer'
Cfg.host.value = 'http://' + window.location.host
Cfg.token.value = localStorage.getItem('auth_token') || ''

@ -56,7 +56,7 @@ import { useRouter } from 'vue-router';
import Menu from 'src/components/menu.vue'
import { useAppStore } from 'src/stores/app';
import { useUserStore } from 'src/stores/user';
import { OAer } from "src/oaer";
import { OAer } from "@veypi/oaer";
const app = useAppStore()
const user = useUserStore()

@ -1,48 +0,0 @@
/*
* @name: app
* @author: veypi <i@veypi.com>
* @date: 2021-11-17 14:44
* @descriptionap
* @update: 2021-11-17 14:44
*/
import ajax from './axios'
import { Cfg } from './setting'
export default {
local: () => Cfg.BaseUrl() + '/app/',
self() {
return ajax.get(this.local(), { option: 'oa' })
},
getKey(uuid: string) {
return ajax.get(this.local() + uuid, { option: 'key' })
},
create(name: string, icon: string) {
return ajax.post(this.local(), { name, icon })
},
get(uuid: string) {
return ajax.get(this.local() + uuid)
},
list() {
return ajax.get(this.local())
},
update(uuid: string, props: any) {
return ajax.patch(this.local() + uuid, props)
},
user(uuid: string) {
if (uuid === '') {
uuid = '-'
}
return {
local: () => this.local() + uuid + '/user/',
list(id: string) {
return ajax.get(this.local() + id)
},
add(uid: number) {
return ajax.post(this.local() + uid)
},
update(uid: number, status: string) {
return ajax.patch(this.local() + uid, { status })
},
}
},
}

@ -1,86 +0,0 @@
/*
* axios.ts
* Copyright (C) 2023 veypi <i@veypi.com>
* 2023-09-22 20:22
* Distributed under terms of the MIT license.
*/
import axios, { AxiosError, AxiosInstance, AxiosResponse } from 'axios';
import msg from '@veypi/msg'
// Be careful when using SSR for cross-request state pollution
// due to creating a Singleton instance here;
// If any client changes this (global) instance, it might be a
// good idea to move this instance creation inside of the
// "export default () => {}" function below (which runs individually
// for each client)
const proxy = axios.create({
withCredentials: true,
headers: {
'content-type': 'application/json;charset=UTF-8',
},
});
// 请求拦截
const beforeRequest = (config: any) => {
// 设置 token
const token = localStorage.getItem('auth_token')
// NOTE 添加自定义头部
token && (config.headers.auth_token = token)
// config.headers['auth_token'] = ''
return config
}
proxy.interceptors.request.use(beforeRequest)
// 响应拦截器
const responseSuccess = (response: AxiosResponse) => {
// eslint-disable-next-line yoda
// 这里没有必要进行判断axios 内部已经判断
// const isOk = 200 <= response.status && response.status < 300
let data = response.data
if (response.config.method === 'head') {
data = JSON.parse(JSON.stringify(response.headers))
}
return Promise.resolve(data)
}
const responseFailed = (error: AxiosError) => {
const { response } = error
if (!window.navigator.onLine) {
alert('没有网络')
return Promise.reject(new Error('请检查网络连接'))
}
console.log(response)
return Promise.reject(response?.data || response?.headers.error)
}
proxy.interceptors.response.use(responseSuccess, responseFailed)
const ajax = {
get(url: string, data = {}, header?: any) {
return proxy.get<any, any>(url, { params: data, headers: header })
},
head(url: string, data = {}, header?: any) {
return proxy.head<any, any>(url, { params: data, headers: header })
},
delete(url: string, data = {}, header?: any) {
return proxy.delete<any, any>(url, { params: data, headers: header })
},
post(url: string, data = {}, header?: any) {
return proxy.post<any, any>(url, data, { headers: header })
},
put(url: string, data = {}, header?: any) {
return proxy.put<any, any>(url, data, { headers: header })
},
patch(url: string, data = {}, header?: any) {
return proxy.patch<any, any>(url, data, { headers: header })
},
}
export default ajax

@ -1,18 +0,0 @@
/*
* 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

@ -1,28 +0,0 @@
/*
* @name: setting
* @author: veypi <i@veypi.com>
* @date: 2021-11-17 15:45
* @descriptionsetting
* @update: 2021-11-17 15:45
*/
import { ref } from 'vue'
export let Cfg = {
token: ref(''),
uuid: ref(''),
host: ref(''),
prefix: '/api',
BaseUrl() {
return this.host.value + this.prefix
},
goto(url: string) {
if (!url.startsWith('/')) {
url = '/' + url
}
window.location.href = this.host.value + '/#' + url
},
userFileUrl() {
return (this.host.value || window.location.host) + '/file/'
},
}

@ -1,44 +0,0 @@
/*
* user.ts
* Copyright (C) 2023 veypi <i@veypi.com>
* 2023-10-05 15:37
* Distributed under terms of the MIT license.
*/
import { Base64 } from 'js-base64'
import ajax from './axios'
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 ajax.post(this.local(), data)
},
login(username: string, password: string) {
return ajax.head(this.local() + username, {
typ: 'username',
password: Base64.encode(password),
})
},
search(q: string) {
return ajax.get(this.local(), { username: q })
},
get(id: number) {
return ajax.get(this.local() + id)
},
list() {
return ajax.get(this.local())
},
update(id: number, props: any) {
return ajax.patch(this.local() + id, props)
},
}

@ -1 +0,0 @@
!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);

@ -1,26 +0,0 @@
<template>
<div class="w-full px-3">
<div class="h-16 flex justify-between items-center">
<span style="">我的应用</span>
<span @click="Cfg.goto('/')" 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="Cfg.goto(ap.redirect)" v-if="ap.id !== Cfg.uuid.value">
<img class="oa_avator" v-if="ap.icon" :src="Cfg.host.value +
ap.icon" alt="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'
withDefaults(defineProps<{
apps: modelsApp[]
}>(), {})
</script>
<style scoped></style>

@ -1,47 +0,0 @@
<template>
<div class="w-full px-3">
<div class="h-16 flex justify-between items-center">
<span style="">
我的云盘
</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">
<div @click="showModal = true" type="primary">获取挂载链接</div>
</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(Cfg.userFileUrl(),
{ headers: { auth_token: Cfg.token.value as string } })
onMounted(() => {
client.stat('').then((e) => {
console.log(e)
}).catch((e) => {
console.log(e)
})
})
</script>
<style scoped></style>

@ -1,7 +0,0 @@
<template>
<div></div>
</template>
<script lang="js" setup>
</script>
<style scoped>
</style>

@ -1,12 +0,0 @@
/*
* @name: index
* @author: veypi <i@veypi.com>
* @date: 2021-12-18 14:24
* @descriptionindex
*/
import mitt from 'mitt'
const emitter = mitt()
export default emitter

@ -1,90 +0,0 @@
<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>

@ -1,14 +0,0 @@
/*
* @name: index
* @author: veypi <i@veypi.com>
* @date: 2021-12-18 13:16
* @descriptionindex
*/
import 'animate.css'
import OAer from './main.vue'
import './assets/icon.js'
import { Cfg, api } from './api'
export { OAer, Cfg, api }

@ -1,7 +0,0 @@
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}`;
}

@ -1,82 +0,0 @@
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;
}

@ -1,36 +0,0 @@
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}`
);
}
}

@ -1,5 +0,0 @@
import { AuthHeader, OAuthToken } from "../types";
export function generateTokenAuthHeader(token: OAuthToken): AuthHeader {
return `${token.token_type} ${token.access_token}`;
}

@ -1,10 +0,0 @@
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]")
);
}

@ -1,8 +0,0 @@
export function isBuffer(value: any): boolean {
return (
value != null &&
value.constructor != null &&
typeof value.constructor.isBuffer === "function" &&
value.constructor.isBuffer(value)
);
}

@ -1,10 +0,0 @@
import HotPatcher from "hot-patcher";
let __patcher: HotPatcher = null;
export function getPatcher(): HotPatcher {
if (!__patcher) {
__patcher = new HotPatcher();
}
return __patcher;
}

@ -1,114 +0,0 @@
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)
};
}

@ -1,5 +0,0 @@
export { createClient } from "./factory";
export { getPatcher } from "./compat/patcher";
export * from "./types";
export { parseStat, parseXML } from "./tools/dav";

@ -1,26 +0,0 @@
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);
}

@ -1,81 +0,0 @@
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;
}
}
}
}

@ -1,109 +0,0 @@
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;
}

@ -1,19 +0,0 @@
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;
}

@ -1,22 +0,0 @@
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);
}

@ -1,78 +0,0 @@
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(/\/$/, ""))
)
);
}

@ -1,18 +0,0 @@
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;
}
}

@ -1,102 +0,0 @@
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;
}

@ -1,30 +0,0 @@
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);
}

@ -1,79 +0,0 @@
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;
}
}

@ -1,26 +0,0 @@
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);
}

@ -1,94 +0,0 @@
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;
}

@ -1,32 +0,0 @@
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);
}

@ -1,108 +0,0 @@
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;
});
}

@ -1,46 +0,0 @@
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;
}

@ -1,16 +0,0 @@
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;
}

@ -1,171 +0,0 @@
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);
}
}

@ -1,24 +0,0 @@
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);
}

@ -1,18 +0,0 @@
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;
}, {});
}

@ -1,62 +0,0 @@
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;
}

@ -1,36 +0,0 @@
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;
}

@ -1,22 +0,0 @@
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;
}

@ -1,22 +0,0 @@
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"
);
}

@ -1,32 +0,0 @@
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;
}

@ -1,55 +0,0 @@
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
});
}

@ -1,288 +0,0 @@
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;
}

@ -1,135 +0,0 @@
<template>
<BaseFrame :class="[isDark ? 'oa_light' : 'oa_dark']" v-model="shown" :is-dark="isDark">
<template #title>
{{ self.name }}
</template>
<div class="flex justify-center items-center">
<img class="oa_avatar mx-2" :src="Cfg.host.value + usr.icon" alt="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="">我的账户</span>
<span @click="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">
<img class="oa_avatar mx-2" :src="Cfg.host.value + usr.icon" alt="Avatar" />
</div>
<div class="col-span-2 text-xs grid grid-cols-1 items-center text-left" style="">
<span>昵称: &ensp;&ensp; {{ usr.nickname }}</span>
<span>账户: &ensp;&ensp; {{ usr.username }}</span>
<span>邮箱: &ensp;&ensp; {{ 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 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]))
console.log(data)
if (data.id) {
api.user.get(data.id).then(e => {
console.log(e)
usr.value = e
ofApps.value = []
for (let v of e.Apps) {
if (v.Status === 'ok') {
ofApps.value.push(v.App)
}
if (v.App.id === Cfg.uuid.value) {
self.value = v.App
}
}
emits('load', e)
}).catch(e => {
console.log(e)
evt.emit('logout')
})
} else {
evt.emit('logout')
}
}
evt.on('logout', () => {
emits('logout')
})
</script>
<style>
.oa_light {
color: #eee;
}
.oa_dark {
color: #333;
}
.oa_avatar {
vertical-align: middle;
width: 2.5rem;
height: 2.5rem;
border-radius: 50%;
}
</style>

@ -1,53 +0,0 @@
/*
* @name: index
* @author: veypi <i@veypi.com>
* @date: 2021-11-18 17:36
* @descriptionindex
*/
export interface modelsApp {
created: string
updated: string
delete_flag: boolean
des: string
hide: boolean
icon: string
id: string
name: string
redirect: string
role_id: string
status: number
user_count: number
au: modelsAppUser
}
export enum AUStatus {
OK = 0,
Disabled = 1,
Applying = 2,
Deny = 3,
}
export interface modelsAppUser {
app_id: string
user_id: string
status: AUStatus
}
export interface modelsUser {
id: string
created: string
updated: string
delete_flag: boolean
username: string
nickname: string
email: string
phone: string
icon: string
status: number
used: number
space: number
}

@ -1,10 +0,0 @@
/* eslint-disable */
/// <reference types="vite/client" />
// Mocks all files ending in `.vue` showing them as plain Vue instances
declare module '*.vue' {
import type { DefineComponent } from 'vue';
const component: DefineComponent<{}, {}, any>;
export default component;
}

@ -16,7 +16,7 @@
</div>
<div class="mt-20" v-if="apps.length > 0">
<h1 class="page-h1">应用中心</h1>
<div class="grid gap-4 grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5 text-center">
<div class="grid gap-4 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5 text-center">
<div v-for="(item, k) in apps" class="flex items-center justify-center" :key="k">
<AppCard :core="item"></AppCard>
</div>

@ -465,8 +465,10 @@
resolved "https://registry.yarnpkg.com/@veypi/msg/-/msg-0.1.0.tgz#2ebe899527a11ed11f68c2c96f468cfcc66ad3d4"
integrity sha512-58dj5nnpHsxaiK5sbPiDK5t8OF4uvN+kAmWhU0BRAgXHpkxkZNZ8rn7hXvSVybG1BbM8EuMNkq0lIxGYNKl8aw==
"@veypi/oaer@file:../../../test/oaer":
"@veypi/oaer@^0.0.1":
version "0.0.1"
resolved "https://registry.yarnpkg.com/@veypi/oaer/-/oaer-0.0.1.tgz#b22ebaf72a7bfd5abf62f099b72b533a8cf27abd"
integrity sha512-ILY8SXK7yihH2/qhUFfPbD2FNE7O4IHh7Q9Z1A+Ild6zonCb3RM7LcOBM4/9WriEi3mPdBNmFfuoP8YFCIxHuA==
dependencies:
"@veypi/msg" "^0.1.0"
"@veypi/one-icon" "2"
@ -474,8 +476,6 @@
autoprefixer "^10.4.16"
axios "^1.5.1"
js-base64 "^3.7.5"
mitt "^3.0.1"
path "^0.12.7"
postcss "^8.4.31"
tailwindcss "^3.3.3"
vue "^3.3.4"
@ -2098,11 +2098,6 @@ inherits@2, inherits@2.0.4, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.3:
resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c"
integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==
inherits@2.0.3:
version "2.0.3"
resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de"
integrity sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==
inquirer@^8.2.1:
version "8.2.6"
resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-8.2.6.tgz#733b74888195d8d400a67ac332011b5fae5ea562"
@ -2744,14 +2739,6 @@ path-type@^4.0.0:
resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b"
integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==
path@^0.12.7:
version "0.12.7"
resolved "https://registry.yarnpkg.com/path/-/path-0.12.7.tgz#d4dc2a506c4ce2197eb481ebfcd5b36c0140b10f"
integrity sha512-aXXC6s+1w7otVF9UletFkFcDsJeO7lSZBPUQhtb5O0xJe8LtYhj/GxldoL09bBj9+ZmE2hNoHqQSFMN5fikh4Q==
dependencies:
process "^0.11.1"
util "^0.10.3"
picocolors@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c"
@ -2857,11 +2844,6 @@ process-nextick-args@~2.0.0:
resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2"
integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==
process@^0.11.1:
version "0.11.10"
resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182"
integrity sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==
proxy-addr@~2.0.7:
version "2.0.7"
resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.7.tgz#f19fe69ceab311eeb94b42e70e8c2070f9ba1025"
@ -3477,13 +3459,6 @@ util-deprecate@^1.0.1, util-deprecate@^1.0.2, util-deprecate@~1.0.1:
resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==
util@^0.10.3:
version "0.10.4"
resolved "https://registry.yarnpkg.com/util/-/util-0.10.4.tgz#3aa0125bfe668a4672de58857d3ace27ecb76901"
integrity sha512-0Pm9hTQ3se5ll1XihRic3FDIku70C+iHUdT/W926rSgHV5QgXsYbKZN8MSC3tJtSkhuROzvsQjAaFENRXr+19A==
dependencies:
inherits "2.0.3"
utils-merge@1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713"

Loading…
Cancel
Save