Compare commits
2 Commits
7bedf7c13f
...
01a6481d62
Author | SHA1 | Date | |
---|---|---|---|
01a6481d62 | |||
65a67ae19b |
1
.env.example
Normal file
1
.env.example
Normal file
@ -0,0 +1 @@
|
|||||||
|
VUE_APP_ROOT_WEBDAV="http://127.0.0.1:8080"
|
64
docker-compose.yml
Normal file
64
docker-compose.yml
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
version: '3'
|
||||||
|
services:
|
||||||
|
webdav:
|
||||||
|
build:
|
||||||
|
context: ./docker/webdav
|
||||||
|
dockerfile: ./Dockerfile
|
||||||
|
restart: always
|
||||||
|
volumes:
|
||||||
|
- ./nginx.conf:/etc/nginx/nginx.conf
|
||||||
|
- ./htpasswd:/etc/nginx/htpasswd
|
||||||
|
- ./dist/:/var/www/html/
|
||||||
|
- ./media/:/media/
|
||||||
|
ports:
|
||||||
|
- "8080:8080"
|
||||||
|
links:
|
||||||
|
- ldap
|
||||||
|
- nginx-ldap-auth
|
||||||
|
|
||||||
|
nginx-ldap-auth:
|
||||||
|
image: bitnami/nginx-ldap-auth-daemon
|
||||||
|
restart: always
|
||||||
|
links:
|
||||||
|
- ldap
|
||||||
|
|
||||||
|
ldap:
|
||||||
|
image: mwaeckerlin/openldap
|
||||||
|
ports:
|
||||||
|
- "389:389"
|
||||||
|
volumes:
|
||||||
|
- ./docker./ldap/:/var/restore/
|
||||||
|
environment:
|
||||||
|
DOMAIN: example.com
|
||||||
|
DEBUG: 256
|
||||||
|
ACCESS_RULES: |
|
||||||
|
access to attrs=userPassword
|
||||||
|
by anonymous auth
|
||||||
|
by self write
|
||||||
|
by * none
|
||||||
|
access to *
|
||||||
|
by * read
|
||||||
|
restart: always
|
||||||
|
healthcheck:
|
||||||
|
test: "ldapsearch -x -b dc=example,dc=com cn > /dev/null"
|
||||||
|
interval: 30s
|
||||||
|
retries: 2
|
||||||
|
timeout: 2s
|
||||||
|
|
||||||
|
ldap-ui:
|
||||||
|
image: dnknth/ldap-ui
|
||||||
|
ports:
|
||||||
|
- "5000:5000"
|
||||||
|
links:
|
||||||
|
- ldap
|
||||||
|
environment:
|
||||||
|
LDAP_URL: "ldap://ldap/"
|
||||||
|
BASE_DN: "dc=example,dc=com"
|
||||||
|
BIND_DN: "cn=admin,dc=example,dc=com"
|
||||||
|
BIND_PASSWORD: "admin"
|
||||||
|
restart: always
|
||||||
|
healthcheck:
|
||||||
|
test: "wget -q -O /dev/null http://localhost:5000"
|
||||||
|
interval: 30s
|
||||||
|
retries: 2
|
||||||
|
timeout: 2s
|
32
docker/ldap/example.ldif
Normal file
32
docker/ldap/example.ldif
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
# Entry 1: dc=example,dc=com
|
||||||
|
dn: dc=example,dc=com
|
||||||
|
dc: example
|
||||||
|
o: Example
|
||||||
|
objectclass: dcObject
|
||||||
|
objectclass: top
|
||||||
|
objectclass: organization
|
||||||
|
|
||||||
|
dn: cn=admin,dc=example,dc=com
|
||||||
|
cn: admin
|
||||||
|
uid: admin
|
||||||
|
userpassword: admin
|
||||||
|
objectclass: organizationalRole
|
||||||
|
objectclass: simpleSecurityObject
|
||||||
|
objectclass: uidObject
|
||||||
|
|
||||||
|
# Entry 2: ou=users,dc=example,dc=com
|
||||||
|
dn: ou=users,dc=example,dc=com
|
||||||
|
objectclass: organizationalUnit
|
||||||
|
objectclass: top
|
||||||
|
ou: users
|
||||||
|
|
||||||
|
# Entry 3: cn=Test User,ou=users,dc=example,dc=com
|
||||||
|
dn: cn=Test User,ou=users,dc=example,dc=com
|
||||||
|
cn: Test User
|
||||||
|
givenname: Test User
|
||||||
|
objectclass: inetOrgPerson
|
||||||
|
objectclass: uidObject
|
||||||
|
objectclass: simpleSecurityObject
|
||||||
|
sn: User
|
||||||
|
uid: test
|
||||||
|
userPassword: test
|
29
docker/webdav/Dockerfile
Normal file
29
docker/webdav/Dockerfile
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
FROM alpine
|
||||||
|
|
||||||
|
RUN apk update && \
|
||||||
|
apk add --no-cache pcre libxml2 libxslt && \
|
||||||
|
apk add --no-cache apache2-utils && \
|
||||||
|
apk add --no-cache gcc make libc-dev pcre-dev zlib-dev libxml2-dev libxslt-dev && \
|
||||||
|
cd /tmp && \
|
||||||
|
wget https://github.com/nginx/nginx/archive/master.zip -O nginx.zip && \
|
||||||
|
unzip nginx.zip && \
|
||||||
|
wget https://github.com/arut/nginx-dav-ext-module/archive/master.zip -O dav-ext-module.zip && \
|
||||||
|
unzip dav-ext-module.zip && \
|
||||||
|
cd nginx-master && \
|
||||||
|
./auto/configure --prefix=/opt/nginx --with-http_dav_module --with-http_auth_request_module --add-module=/tmp/nginx-dav-ext-module-master && \
|
||||||
|
make && make install && \
|
||||||
|
cd /root && \
|
||||||
|
apk del gcc make libc-dev pcre-dev zlib-dev libxml2-dev libxslt-dev && \
|
||||||
|
rm -rf /var/cache/apk/* && \
|
||||||
|
rm -rf /tmp/*
|
||||||
|
|
||||||
|
RUN mkdir -p /tmp/nginx/client-body
|
||||||
|
COPY nginx.conf /opt/nginx/conf/nginx.conf
|
||||||
|
COPY htpasswd /opt/nginx/htpasswd
|
||||||
|
|
||||||
|
#RUN apk update && \
|
||||||
|
# apk add nginx nginx-extras
|
||||||
|
|
||||||
|
EXPOSE 8080
|
||||||
|
|
||||||
|
CMD /bin/echo "starting nginx webdav server" && /opt/nginx/sbin/nginx -g "daemon off;"
|
1
docker/webdav/htpasswd
Normal file
1
docker/webdav/htpasswd
Normal file
@ -0,0 +1 @@
|
|||||||
|
test2:$apr1$zSKjrvfS$r6itS4PfhS2QicesM70Ks/
|
97
docker/webdav/nginx.conf
Normal file
97
docker/webdav/nginx.conf
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
worker_processes auto;
|
||||||
|
worker_cpu_affinity auto;
|
||||||
|
|
||||||
|
#pid /var/run/nginx.pid;
|
||||||
|
error_log /dev/stderr warn;
|
||||||
|
|
||||||
|
events {
|
||||||
|
worker_connections 1024;
|
||||||
|
}
|
||||||
|
|
||||||
|
http {
|
||||||
|
# rewrite_log on;
|
||||||
|
include mime.types;
|
||||||
|
default_type application/json;
|
||||||
|
access_log /dev/stdout;
|
||||||
|
sendfile on;
|
||||||
|
# tcp_nopush on;
|
||||||
|
keepalive_timeout 3;
|
||||||
|
# tcp_nodelay on;
|
||||||
|
gzip on;
|
||||||
|
|
||||||
|
proxy_cache_path ./cache/ keys_zone=auth_cache:5m;
|
||||||
|
client_max_body_size 1M;
|
||||||
|
|
||||||
|
server {
|
||||||
|
listen 8080 default_server;
|
||||||
|
server_name _;
|
||||||
|
|
||||||
|
absolute_redirect off;
|
||||||
|
|
||||||
|
location / {
|
||||||
|
root /var/www/html;
|
||||||
|
index index.html;
|
||||||
|
|
||||||
|
try_files $uri $uri/ /index.html;
|
||||||
|
}
|
||||||
|
|
||||||
|
location = /ldap-auth {
|
||||||
|
internal;
|
||||||
|
proxy_pass_request_body off;
|
||||||
|
client_max_body_size 0; # has to be set even tho the body is not passed
|
||||||
|
proxy_set_header Content-Length "";
|
||||||
|
#proxy_cache auth_cache;
|
||||||
|
#proxy_cache_valid 200 5m;
|
||||||
|
#proxy_cache_key $scheme$proxy_host$request_uri$remote_user;
|
||||||
|
proxy_pass http://nginx-ldap-auth:8888;
|
||||||
|
proxy_set_header X-Ldap-URL "ldap://ldap/";
|
||||||
|
proxy_set_header X-Ldap-Template "(uid=%(username)s)";
|
||||||
|
proxy_set_header X-Ldap-BaseDN "ou=users,dc=example,dc=com";
|
||||||
|
#proxy_set_header X-Ldap-BindDN "cn=test,dc=example,dc=com";
|
||||||
|
#proxy_set_header X-Ldap-BindPass "test";
|
||||||
|
}
|
||||||
|
|
||||||
|
#location ~ ^/api/dav/files/(?<userpath>(\w+))(|(?<filename>/.*))$ {
|
||||||
|
location ~ ^/api/dav/files(?<filename>.*)$ {
|
||||||
|
|
||||||
|
if ( $request_method = OPTIONS ) {
|
||||||
|
add_header "Access-Control-Allow-Origin" *;
|
||||||
|
add_header "Access-Control-Allow-Methods" *;
|
||||||
|
add_header "Access-Control-Allow-Headers" "Authorization, Origin, X-Requested-With, Content-Type, Accept";
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($remote_user = "") {
|
||||||
|
add_header "WWW-Authenticate" "Basic realm=\"Restricted\"";
|
||||||
|
return 401;
|
||||||
|
}
|
||||||
|
|
||||||
|
proxy_set_header X-Auth "nginxauth";
|
||||||
|
proxy_set_header Cookie nginxauth=$cookie_nginxauth;
|
||||||
|
proxy_set_header Authorization $http_authorization;
|
||||||
|
|
||||||
|
auth_request /ldap-auth;
|
||||||
|
auth_request_set $new_cookie $sent_http_set_cookie;
|
||||||
|
|
||||||
|
add_header "Set-Cookie" $new_cookie;
|
||||||
|
add_header "X-Auth" $sent_http_set_cookie;
|
||||||
|
auth_basic "Restricted";
|
||||||
|
#auth_basic_user_file /opt/nginx/htpasswd;
|
||||||
|
satisfy any;
|
||||||
|
|
||||||
|
alias /media/$remote_user$filename;
|
||||||
|
|
||||||
|
client_max_body_size 120G;
|
||||||
|
client_body_temp_path /tmp/nginx/client-body;
|
||||||
|
create_full_put_path on;
|
||||||
|
autoindex on;
|
||||||
|
autoindex_exact_size off;
|
||||||
|
autoindex_localtime on;
|
||||||
|
autoindex_format html;
|
||||||
|
charset utf-8;
|
||||||
|
|
||||||
|
dav_methods PUT DELETE MKCOL COPY MOVE;
|
||||||
|
dav_ext_methods PROPFIND OPTIONS;
|
||||||
|
dav_access user:rw group:rw all:rw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -9,11 +9,15 @@
|
|||||||
"lint": "vue-cli-service lint"
|
"lint": "vue-cli-service lint"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"bootstrap": "^5.2.0",
|
||||||
|
"bootstrap-darkmode": "^5.0.1",
|
||||||
|
"bootstrap-icons": "^1.9.1",
|
||||||
"core-js": "^3.6.5",
|
"core-js": "^3.6.5",
|
||||||
|
"pinia": "^2.0.13",
|
||||||
"register-service-worker": "^1.7.1",
|
"register-service-worker": "^1.7.1",
|
||||||
|
"typescript-is": "^0.19.0",
|
||||||
"vue": "^3.0.0",
|
"vue": "^3.0.0",
|
||||||
"vue-router": "^4.0.0-0",
|
"vue-router": "^4.0.0-0",
|
||||||
"pinia": "^2.0.13",
|
|
||||||
"webdav": "^4.9.0"
|
"webdav": "^4.9.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
44
src/App.vue
44
src/App.vue
@ -1,30 +1,24 @@
|
|||||||
<template>
|
<template>
|
||||||
<div id="nav">
|
<header
|
||||||
<router-link to="/">Home</router-link> |
|
class="d-flex justify-content-between p-3 bg-darkmode-dark bg-light shadow"
|
||||||
<router-link to="/about">About</router-link>
|
>
|
||||||
|
<div>
|
||||||
|
<b class="mx-2">vuedav</b>
|
||||||
|
<router-link class="mx-2" to="/files">Files</router-link>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="d-flex">
|
||||||
|
<DarkModeToggle class="mx-2" v-slot="{ state }">
|
||||||
|
<i v-if="state" class="bi-moon"></i>
|
||||||
|
<i v-else class="bi-sun"></i>
|
||||||
|
</DarkModeToggle>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
<main class="container my-5">
|
||||||
<router-view />
|
<router-view />
|
||||||
|
</main>
|
||||||
|
<footer></footer>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="scss">
|
<script setup lang="ts">
|
||||||
#app {
|
import DarkModeToggle from '@/components/DarkmodeToggle.vue';
|
||||||
font-family: Avenir, Helvetica, Arial, sans-serif;
|
</script>
|
||||||
-webkit-font-smoothing: antialiased;
|
|
||||||
-moz-osx-font-smoothing: grayscale;
|
|
||||||
text-align: center;
|
|
||||||
color: #2c3e50;
|
|
||||||
}
|
|
||||||
|
|
||||||
#nav {
|
|
||||||
padding: 30px;
|
|
||||||
|
|
||||||
a {
|
|
||||||
font-weight: bold;
|
|
||||||
color: #2c3e50;
|
|
||||||
|
|
||||||
&.router-link-exact-active {
|
|
||||||
color: #42b983;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
23
src/components/DarkmodeToggle.vue
Normal file
23
src/components/DarkmodeToggle.vue
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
<template>
|
||||||
|
<div class="form-check form-switch">
|
||||||
|
<label class="custom-control-label" for="darkSwitch">
|
||||||
|
<slot :state="sliderState" />
|
||||||
|
</label>
|
||||||
|
<input v-model="sliderState" type="checkbox" class="form-check-input" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, watch } from 'vue';
|
||||||
|
import { themeConfig } from '@/main';
|
||||||
|
|
||||||
|
const sliderState = ref(themeConfig.getTheme() === 'dark');
|
||||||
|
|
||||||
|
watch(sliderState, (state) => {
|
||||||
|
themeConfig.setTheme(state ? 'dark' : 'light');
|
||||||
|
});
|
||||||
|
|
||||||
|
themeConfig.themeChangeHandlers.push((newTheme) => {
|
||||||
|
sliderState.value = newTheme === 'dark';
|
||||||
|
});
|
||||||
|
</script>
|
19
src/components/SideMenu.vue
Normal file
19
src/components/SideMenu.vue
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
<template>
|
||||||
|
<div class="card sideMenu sticky-top">
|
||||||
|
<div class="card-header">Menu</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<router-link class="py-2" to="/files">
|
||||||
|
<i class="bi-archive"></i> My Files
|
||||||
|
</router-link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.sideMenu {
|
||||||
|
a {
|
||||||
|
color: inherit;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
100
src/components/files/FileList.vue
Normal file
100
src/components/files/FileList.vue
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
<template>
|
||||||
|
<div class="px-0">
|
||||||
|
<div class="files card">
|
||||||
|
<div class="card-header sticky-top bg-light bg-darkmode-dark">
|
||||||
|
<div class="row">
|
||||||
|
<i
|
||||||
|
v-if="path && path !== '/'"
|
||||||
|
@click="navigateUp"
|
||||||
|
class="col-1 px-0 btn btn-default bi-arrow-90deg-left"
|
||||||
|
></i>
|
||||||
|
<PathSegments
|
||||||
|
class="col my-2"
|
||||||
|
:path="path"
|
||||||
|
@newPath="(newPath) => emit('newPath', newPath)"
|
||||||
|
/>
|
||||||
|
<div class="col-auto px-0">
|
||||||
|
<FileUpload :path="path" @finished="fetchLocation" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div v-if="loading">loading...</div>
|
||||||
|
<div v-else>
|
||||||
|
<table class="table table-hover">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th></th>
|
||||||
|
<th>Name</th>
|
||||||
|
<th class="d-none d-md-table-cell">Modified</th>
|
||||||
|
<th class="d-none d-sm-table-cell">Size</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<component
|
||||||
|
v-for="file in files"
|
||||||
|
:key="file"
|
||||||
|
:is="file.type === 'file' ? FileListElement : FolderListElement"
|
||||||
|
:file="file"
|
||||||
|
:client="client"
|
||||||
|
/>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
{{ error }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import FileListElement from './FileListElement.vue';
|
||||||
|
import FolderListElement from './FolderListElement.vue';
|
||||||
|
import FileUpload from '@/components/helpers/FileUpload.vue';
|
||||||
|
import PathSegments from '@/components/files/PathSegments.vue';
|
||||||
|
import { useWebdavStore } from '@/store/webdav';
|
||||||
|
import { ref, defineProps, watch, defineEmits } from 'vue';
|
||||||
|
import type { File } from '@/models';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
path: String,
|
||||||
|
});
|
||||||
|
const emit = defineEmits(['newPath']);
|
||||||
|
|
||||||
|
const files = ref([] as Array<File>);
|
||||||
|
const loading = ref(true as boolean);
|
||||||
|
const error = ref('' as string);
|
||||||
|
const store = useWebdavStore();
|
||||||
|
|
||||||
|
const client = store.currentSession?.client;
|
||||||
|
|
||||||
|
const fetchLocation = async () => {
|
||||||
|
try {
|
||||||
|
loading.value = true;
|
||||||
|
files.value = (await client?.getDirectoryContents(
|
||||||
|
props.path ?? '/'
|
||||||
|
)) as Array<File>;
|
||||||
|
loading.value = false;
|
||||||
|
error.value = '';
|
||||||
|
} catch (e) {
|
||||||
|
files.value = [];
|
||||||
|
loading.value = false;
|
||||||
|
error.value = e as string;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const navigateUp = () => {
|
||||||
|
const newPath = (props.path ?? '/').split('/');
|
||||||
|
newPath.pop();
|
||||||
|
emit('newPath', newPath.join('/') || '/');
|
||||||
|
};
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.path,
|
||||||
|
() => {
|
||||||
|
fetchLocation();
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
fetchLocation();
|
||||||
|
</script>
|
32
src/components/files/FileListElement.vue
Normal file
32
src/components/files/FileListElement.vue
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
<template>
|
||||||
|
<tr>
|
||||||
|
<td><i class="icon col-1" :class="`bi-${getIconFromFile(file)}`"></i></td>
|
||||||
|
<td>{{ file.basename }}</td>
|
||||||
|
<td class="d-none d-md-table-cell">
|
||||||
|
{{ new Date(file.lastmod).toLocaleDateString() }}
|
||||||
|
</td>
|
||||||
|
<td class="d-none d-sm-table-cell"><ByteCalc :bytes="file.size" /></td>
|
||||||
|
</tr>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import ByteCalc from '@/components/helpers/ByteCalc.vue';
|
||||||
|
import { defineProps } from 'vue';
|
||||||
|
import { fileExtensions, defaultIcon } from '@/lib/fileTypeToIconMappings';
|
||||||
|
import type { File } from '@/models';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
file: Object,
|
||||||
|
client: Object,
|
||||||
|
});
|
||||||
|
|
||||||
|
const getIconFromFile = (file: File): string => {
|
||||||
|
const segments = file.basename.split('.');
|
||||||
|
if (segments.length < 2) return defaultIcon;
|
||||||
|
return fileExtensions.get(segments.pop() as string) ?? defaultIcon;
|
||||||
|
};
|
||||||
|
|
||||||
|
const downloadFile = async (file: File) => {
|
||||||
|
await props.client?.getFileContents(file.filename);
|
||||||
|
};
|
||||||
|
</script>
|
27
src/components/files/FolderListElement.vue
Normal file
27
src/components/files/FolderListElement.vue
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
<template>
|
||||||
|
<tr @click="$router.push(`#${file.filename}`)" class="file">
|
||||||
|
<td><i class="icon bi-folder"></i></td>
|
||||||
|
<td>{{ file.basename }}</td>
|
||||||
|
<td class="d-none d-md-table-cell">{{ new Date(file.lastmod).toLocaleDateString() }}</td>
|
||||||
|
<td class="d-none d-sm-table-cell"></td>
|
||||||
|
</tr>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { defineProps } from 'vue';
|
||||||
|
|
||||||
|
defineProps({
|
||||||
|
file: Object,
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.file {
|
||||||
|
color: inherit !important;
|
||||||
|
text-decoration: inherit !important;
|
||||||
|
cursor: pointer;
|
||||||
|
&:hover{
|
||||||
|
color: inherit !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
46
src/components/files/PathSegments.vue
Normal file
46
src/components/files/PathSegments.vue
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<span v-for="(segment, index) of getSegments().slice(0, -1)" :key="segment">
|
||||||
|
<span
|
||||||
|
class="segment p-1"
|
||||||
|
@click="
|
||||||
|
emit(
|
||||||
|
'newPath',
|
||||||
|
`/${getSegments()
|
||||||
|
.slice(1, index + 1)
|
||||||
|
.join('/')}`
|
||||||
|
)
|
||||||
|
"
|
||||||
|
>{{ segment }}</span
|
||||||
|
><i class="bi-caret-right mx-1"></i>
|
||||||
|
</span>
|
||||||
|
<span class="p-1">
|
||||||
|
<b>{{ getSegments()?.pop() }}</b>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { defineProps, defineEmits } from 'vue';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
path: String,
|
||||||
|
});
|
||||||
|
const emit = defineEmits(['newPath']);
|
||||||
|
|
||||||
|
const getSegments = (): Array<string> => {
|
||||||
|
const segments = `My Files${props.path ?? ''}`.split('/');
|
||||||
|
if (segments[segments.length - 1] === '') segments?.pop();
|
||||||
|
return segments;
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.segment {
|
||||||
|
border-radius: .2rem;
|
||||||
|
cursor: pointer;
|
||||||
|
&:hover {
|
||||||
|
background-color: #aaa6;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
23
src/components/helpers/ByteCalc.vue
Normal file
23
src/components/helpers/ByteCalc.vue
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
<template>
|
||||||
|
<span>{{ getByteString(bytes) }}</span>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { defineProps } from 'vue';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
bytes: Number,
|
||||||
|
});
|
||||||
|
|
||||||
|
const getByteString = (bytes: number): string => {
|
||||||
|
const unit = ['B', 'kB', 'MB', 'GB', 'TB', 'PB']
|
||||||
|
.map((symbol, index) => ({
|
||||||
|
symbol,
|
||||||
|
index,
|
||||||
|
breakpoint: 10 ** ((index + 1) * 3),
|
||||||
|
}))
|
||||||
|
.find((unit) => bytes < unit.breakpoint);
|
||||||
|
if (!unit) return 'wtf';
|
||||||
|
return `${(bytes / 10 ** (unit.index * 3)).toFixed(2)} ${unit.symbol}`;
|
||||||
|
};
|
||||||
|
</script>
|
61
src/components/helpers/FileUpload.vue
Normal file
61
src/components/helpers/FileUpload.vue
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
<template>
|
||||||
|
<button class="btn btn-secondary" @click.prevent="$refs.fileInput.click()">
|
||||||
|
<slot><i class="bi-plus"></i></slot>
|
||||||
|
</button>
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
ref="fileInput"
|
||||||
|
class="d-none"
|
||||||
|
@change="setFile($refs.fileInput.files[0])"
|
||||||
|
/>
|
||||||
|
<div v-if="info">{{ info }}</div>
|
||||||
|
<div v-if="progressRef">
|
||||||
|
<ByteCalc :bytes="progressRef.loaded" /><span> of </span
|
||||||
|
><ByteCalc :bytes="progressRef.total" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import ByteCalc from '@/components/helpers/ByteCalc.vue';
|
||||||
|
import { defineEmits, defineProps, ref } from 'vue';
|
||||||
|
import { readFileBuffer } from '@/lib/readFileBlob';
|
||||||
|
import { useWebdavStore } from '@/store/webdav';
|
||||||
|
import type { ProgressEvent } from 'webdav';
|
||||||
|
|
||||||
|
const props = defineProps({ path: String });
|
||||||
|
const emit = defineEmits(['started', 'finished', 'failed', 'progress']);
|
||||||
|
const store = useWebdavStore();
|
||||||
|
|
||||||
|
const progressRef = ref(null as null | ProgressEvent);
|
||||||
|
const info = ref('');
|
||||||
|
|
||||||
|
const setFile = async (file: File) => {
|
||||||
|
emit('started');
|
||||||
|
const buffer = await readFileBuffer(file);
|
||||||
|
try {
|
||||||
|
await store.currentSession?.client.putFileContents(
|
||||||
|
`${props.path}/${file.name}`,
|
||||||
|
buffer,
|
||||||
|
{
|
||||||
|
onUploadProgress: (progress) => {
|
||||||
|
progressRef.value = progress;
|
||||||
|
emit('progress', progress);
|
||||||
|
console.log(`Uploaded ${progress.loaded} bytes of ${progress.total}`);
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
info.value = 'upload completed';
|
||||||
|
emit('finished');
|
||||||
|
} catch (e) {
|
||||||
|
info.value = 'upload failed';
|
||||||
|
emit('failed', e);
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
setTimeout(() => {
|
||||||
|
progressRef.value = null;
|
||||||
|
info.value = '';
|
||||||
|
}, 2000);
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped></style>
|
41
src/lib/fileTypeToIconMappings.ts
Normal file
41
src/lib/fileTypeToIconMappings.ts
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
export const defaultIcon = 'file-earmark';
|
||||||
|
|
||||||
|
export const fileExtensions = new Map([
|
||||||
|
// Images
|
||||||
|
['png', 'file-earmark-image'],
|
||||||
|
['jpg', 'file-earmark-image'],
|
||||||
|
['tiff', 'file-earmark-image'],
|
||||||
|
// Music
|
||||||
|
['mp3', 'file-earmark-music'],
|
||||||
|
['m4a', 'file-earmark-music'],
|
||||||
|
['aac', 'file-earmark-music'],
|
||||||
|
['aiff', 'file-earmark-music'],
|
||||||
|
['wav', 'file-earmark-music'],
|
||||||
|
['wma', 'file-earmark-music'],
|
||||||
|
// Code
|
||||||
|
['html', 'file-earmark-code'],
|
||||||
|
['htm', 'file-earmark-code'],
|
||||||
|
['xml', 'file-earmark-code'],
|
||||||
|
['js', 'file-earmark-code'],
|
||||||
|
['mjs', 'file-earmark-code'],
|
||||||
|
['py', 'file-earmark-code'],
|
||||||
|
['sh', 'file-earmark-code'],
|
||||||
|
['ts', 'file-earmark-code'],
|
||||||
|
['go', 'file-earmark-code'],
|
||||||
|
['rs', 'file-earmark-code'],
|
||||||
|
['java', 'file-earmark-code'],
|
||||||
|
// Binaries
|
||||||
|
['jar', 'file-earmark-binary'],
|
||||||
|
['exe', 'file-earmark-binary'],
|
||||||
|
['iso', 'file-earmark-binary'],
|
||||||
|
// etc...
|
||||||
|
['pdf', 'file-earmark-pdf'],
|
||||||
|
['txt', 'filetype-txt'],
|
||||||
|
['zip', 'file-earmark-zip'],
|
||||||
|
['gz', 'file-earmark-zip'],
|
||||||
|
['xz', 'file-earmark-zip'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
export const mimeTypes = new Map([
|
||||||
|
['application/pdf', 'file-earmark-pdf'],
|
||||||
|
]);
|
41
src/lib/readFileBlob.ts
Normal file
41
src/lib/readFileBlob.ts
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
import { is } from 'typescript-is';
|
||||||
|
|
||||||
|
export const readFileAs = <T>(
|
||||||
|
file: File,
|
||||||
|
getReaderMethod = (reader: FileReader) => reader.readAsDataURL
|
||||||
|
): Promise<T> => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onerror = reject;
|
||||||
|
reader.onload = async (event) => {
|
||||||
|
const buffer = event.target?.result;
|
||||||
|
if (!buffer) reject('failed to read file');
|
||||||
|
if (!is<T>(buffer)) reject('wrong type');
|
||||||
|
else resolve(buffer as T);
|
||||||
|
};
|
||||||
|
getReaderMethod(reader as FileReader)(file);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const readFileBuffer = async (file: File): Promise<ArrayBuffer> => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onerror = reject;
|
||||||
|
reader.onload = async (event) => {
|
||||||
|
const buffer = event.target?.result;
|
||||||
|
if (buffer === null || buffer === undefined || typeof buffer === 'string')
|
||||||
|
reject('failed to read file');
|
||||||
|
else resolve(buffer);
|
||||||
|
};
|
||||||
|
reader.readAsArrayBuffer(file);
|
||||||
|
});
|
||||||
|
/*return readFileAs<ArrayBuffer>(
|
||||||
|
file,
|
||||||
|
(reader: FileReader) => reader.readAsArrayBuffer
|
||||||
|
);*/
|
||||||
|
};
|
||||||
|
|
||||||
|
export const readFileBlob = async (file: File): Promise<Blob> => {
|
||||||
|
const data = await readFileAs<string>(file);
|
||||||
|
return await (await fetch(data)).blob();
|
||||||
|
};
|
@ -1,29 +1,29 @@
|
|||||||
import { Context } from '@/middleware/Context';
|
import { Context } from '@/middleware/Context';
|
||||||
|
|
||||||
type RunMiddleware = (context: Context) => void;
|
type Middleware = (context: Context) => void;
|
||||||
function nextFactory(
|
function nextFactory(
|
||||||
context: Context,
|
context: Context,
|
||||||
middlewares: Array<RunMiddleware>,
|
middlewares: Array<Middleware>,
|
||||||
index: number
|
index: number
|
||||||
) {
|
) {
|
||||||
const subsequentMiddleware = middlewares[index];
|
const subsequentMiddleware = middlewares[index];
|
||||||
if (!subsequentMiddleware) return context.next;
|
if (!subsequentMiddleware) return context.next;
|
||||||
|
|
||||||
return (...args: any[]) => {
|
return (...args: Array<any>) => {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
context.next(...args);
|
context.next(...args as Array<any>);
|
||||||
subsequentMiddleware({
|
subsequentMiddleware({
|
||||||
...context,
|
...context,
|
||||||
next: nextFactory(context, middlewares, index++),
|
next: nextFactory(context, middlewares, index++),
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
export function runMiddleware(context: Context) {
|
export function runMiddleware(context: Context): void {
|
||||||
const { to } = context;
|
const { to } = context;
|
||||||
const middlewares = [
|
const middlewares = [
|
||||||
...((Array.isArray(to.meta.middleware)
|
...((Array.isArray(to.meta.middleware)
|
||||||
? to.meta.middleware
|
? to.meta.middleware
|
||||||
: [to.meta.middleware]) as Array<RunMiddleware>),
|
: [to.meta.middleware]) as Array<Middleware>),
|
||||||
];
|
];
|
||||||
|
|
||||||
middlewares[0]({ ...context, next: nextFactory(context, middlewares, 1) });
|
middlewares[0]({ ...context, next: nextFactory(context, middlewares, 1) });
|
||||||
|
16
src/main.scss
Normal file
16
src/main.scss
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
// Your variable overrides
|
||||||
|
//$body-bg: #222;
|
||||||
|
//$body-color: #222;
|
||||||
|
$dark-body-bg: #222;
|
||||||
|
|
||||||
|
// Bootstrap and its default variables
|
||||||
|
@import "~bootstrap/scss/bootstrap";
|
||||||
|
@import "~bootstrap-darkmode/css/darktheme";
|
||||||
|
|
||||||
|
html, body {
|
||||||
|
}
|
||||||
|
|
||||||
|
#app{
|
||||||
|
font-family: monospace;
|
||||||
|
//color: #fff;
|
||||||
|
}
|
18
src/main.ts
18
src/main.ts
@ -1,7 +1,13 @@
|
|||||||
import { createApp } from "vue";
|
import { createApp } from 'vue';
|
||||||
import App from "./App.vue";
|
import App from './App.vue';
|
||||||
import "./registerServiceWorker";
|
import './registerServiceWorker';
|
||||||
import router from "./router";
|
import router from './router';
|
||||||
import store from "./store";
|
import store from './store';
|
||||||
|
import 'bootstrap-icons/font/bootstrap-icons.scss';
|
||||||
|
import './main.scss';
|
||||||
|
import { ThemeConfig, writeDarkSwitch } from 'bootstrap-darkmode';
|
||||||
|
|
||||||
createApp(App).use(store).use(router).mount("#app");
|
export const themeConfig = new ThemeConfig();
|
||||||
|
themeConfig.initTheme();
|
||||||
|
|
||||||
|
createApp(App).use(store).use(router).mount('#app');
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
import { useWebdavStorage } from '@/store/webdav';
|
import { useWebdavStore } from '@/store/webdav';
|
||||||
import { Context } from '@/middleware/Context';
|
import { Context } from '@/middleware/Context';
|
||||||
|
|
||||||
export default function auth({ next, router }: Context) {
|
export const auth = ({ next }: Context) => {
|
||||||
console.log('auth');
|
if (!useWebdavStore().currentSession?.isActive)
|
||||||
if (!useWebdavStorage().currentSession?.isActive) return router.push({ name: 'Login' });
|
return next({ name: 'Login' });
|
||||||
return next();
|
return next();
|
||||||
}
|
};
|
||||||
|
9
src/models.ts
Normal file
9
src/models.ts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
export type File = {
|
||||||
|
basename: string;
|
||||||
|
etag: string;
|
||||||
|
filename: string;
|
||||||
|
lastmod: string;
|
||||||
|
mime: string;
|
||||||
|
size: number;
|
||||||
|
type: string;
|
||||||
|
};
|
@ -1,7 +1,6 @@
|
|||||||
import { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router';
|
import { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router';
|
||||||
import Home from '../views/Home.vue';
|
import Home from '../views/Home.vue';
|
||||||
import auth from '@/middleware/auth';
|
import { auth } from '@/middleware/auth';
|
||||||
import log from '@/middleware/log';
|
|
||||||
import { runMiddleware } from '@/lib/runMiddleware';
|
import { runMiddleware } from '@/lib/runMiddleware';
|
||||||
|
|
||||||
const routes: Array<RouteRecordRaw> = [
|
const routes: Array<RouteRecordRaw> = [
|
||||||
@ -22,9 +21,6 @@ const routes: Array<RouteRecordRaw> = [
|
|||||||
path: '/login',
|
path: '/login',
|
||||||
name: 'Login',
|
name: 'Login',
|
||||||
component: () => import('../views/Login.vue'),
|
component: () => import('../views/Login.vue'),
|
||||||
meta: {
|
|
||||||
middleware: [log],
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
@ -1,13 +0,0 @@
|
|||||||
import {defineStore} from "pinia";
|
|
||||||
|
|
||||||
export const useAuthStorage = defineStore('auth', {
|
|
||||||
state: () => ({
|
|
||||||
user: '',
|
|
||||||
password: ''
|
|
||||||
}),
|
|
||||||
actions: {
|
|
||||||
login(){
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
@ -11,19 +11,18 @@ export type Session = {
|
|||||||
isActive: boolean;
|
isActive: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useWebdavStorage = defineStore('auth', {
|
export const useWebdavStore = defineStore('auth', {
|
||||||
state: () => ({
|
state: () => ({
|
||||||
sessions: [] as Array<Session>,
|
sessions: [] as Array<Session>,
|
||||||
currentSession: null as null | Session,
|
currentSession: null as null | Session,
|
||||||
}),
|
}),
|
||||||
actions: {
|
actions: {
|
||||||
login({ user, pass }: Auth): Promise<Session> {
|
async login({ user, pass }: Auth): Promise<Session> {
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
try {
|
try {
|
||||||
const client = createClient(
|
const client = createClient(
|
||||||
process.env.VUE_APP_ROOT_WEBDAV as string,
|
`${process.env.VUE_APP_ROOT_WEBDAV ?? ''}/api/dav/files/` as string,
|
||||||
{
|
{
|
||||||
authType: AuthType.Digest,
|
authType: AuthType.Password,
|
||||||
username: user as string,
|
username: user as string,
|
||||||
password: pass as string,
|
password: pass as string,
|
||||||
}
|
}
|
||||||
@ -31,11 +30,10 @@ export const useWebdavStorage = defineStore('auth', {
|
|||||||
const session = { client, isActive: true } as Session;
|
const session = { client, isActive: true } as Session;
|
||||||
this.sessions.push(session);
|
this.sessions.push(session);
|
||||||
this.currentSession = session;
|
this.currentSession = session;
|
||||||
resolve(session);
|
return session;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
reject(e);
|
throw 'login failed';
|
||||||
}
|
}
|
||||||
});
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
@ -1,11 +1,20 @@
|
|||||||
<template>
|
<template>
|
||||||
<h1>Files</h1>
|
<div class="row mx-1 justify-content-center">
|
||||||
|
<div class="d-none d-lg-block col-3">
|
||||||
|
<SideMenu />
|
||||||
|
</div>
|
||||||
|
<FileList
|
||||||
|
class="col"
|
||||||
|
:path="$route.hash.slice(1)"
|
||||||
|
@newPath="(path) => $router.push(`#${path}`)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup lang="ts">
|
||||||
export default {
|
import FileList from '@/components/files/FileList.vue';
|
||||||
name: 'Files',
|
import SideMenu from '@/components/SideMenu.vue';
|
||||||
};
|
import { useWebdavStore } from '@/store/webdav';
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped></style>
|
const store = useWebdavStore();
|
||||||
|
</script>
|
||||||
|
@ -2,22 +2,32 @@
|
|||||||
<h1>Login</h1>
|
<h1>Login</h1>
|
||||||
user: <input type="text" v-model="user" /><br />
|
user: <input type="text" v-model="user" /><br />
|
||||||
pass: <input type="password" v-model="pass" /><br />
|
pass: <input type="password" v-model="pass" /><br />
|
||||||
<button @click="login">Login</button>
|
<button class="btn btn-primary" @click="login">Login</button><br />
|
||||||
|
<span class="error">{{ error }}</span>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref } from 'vue';
|
import { ref } from 'vue';
|
||||||
import { useWebdavStorage } from '@/store/webdav';
|
import { useWebdavStore } from '@/store/webdav';
|
||||||
|
import { useRouter } from 'vue-router';
|
||||||
|
|
||||||
const user = ref('');
|
const user = ref('');
|
||||||
const pass = ref('');
|
const pass = ref('');
|
||||||
|
const error = ref('');
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
const store = useWebdavStore();
|
||||||
|
|
||||||
const login = async () => {
|
const login = async () => {
|
||||||
await useWebdavStorage()
|
try {
|
||||||
.login({ user, pass })
|
await store.login({ user: user.value, pass: pass.value });
|
||||||
.then(async session => {
|
error.value = '';
|
||||||
console.log(await session.client.getDirectoryContents('/'));
|
await router.push({ name: 'Files' });
|
||||||
});
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
error.value = e as string;
|
||||||
|
}
|
||||||
|
await store.currentSession?.client.getDirectoryContents(`/`);
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user