Compare commits
No commits in common. "01a6481d6223c73e9b232853629f1cd3e6b41b70" and "7bedf7c13fea66fde709bbdaad0475edb7f1f609" have entirely different histories.
01a6481d62
...
7bedf7c13f
@ -1 +0,0 @@
|
|||||||
VUE_APP_ROOT_WEBDAV="http://127.0.0.1:8080"
|
|
@ -1,64 +0,0 @@
|
|||||||
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
|
|
@ -1,32 +0,0 @@
|
|||||||
# 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
|
|
@ -1,29 +0,0 @@
|
|||||||
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 +0,0 @@
|
|||||||
test2:$apr1$zSKjrvfS$r6itS4PfhS2QicesM70Ks/
|
|
@ -1,97 +0,0 @@
|
|||||||
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,15 +9,11 @@
|
|||||||
"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": {
|
||||||
|
48
src/App.vue
48
src/App.vue
@ -1,24 +1,30 @@
|
|||||||
<template>
|
<template>
|
||||||
<header
|
<div id="nav">
|
||||||
class="d-flex justify-content-between p-3 bg-darkmode-dark bg-light shadow"
|
<router-link to="/">Home</router-link> |
|
||||||
>
|
<router-link to="/about">About</router-link>
|
||||||
<div>
|
</div>
|
||||||
<b class="mx-2">vuedav</b>
|
<router-view />
|
||||||
<router-link class="mx-2" to="/files">Files</router-link>
|
|
||||||
</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 />
|
|
||||||
</main>
|
|
||||||
<footer></footer>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<style lang="scss">
|
||||||
import DarkModeToggle from '@/components/DarkmodeToggle.vue';
|
#app {
|
||||||
</script>
|
font-family: Avenir, Helvetica, Arial, sans-serif;
|
||||||
|
-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>
|
||||||
|
@ -1,23 +0,0 @@
|
|||||||
<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>
|
|
@ -1,19 +0,0 @@
|
|||||||
<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>
|
|
@ -1,100 +0,0 @@
|
|||||||
<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>
|
|
@ -1,32 +0,0 @@
|
|||||||
<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>
|
|
@ -1,27 +0,0 @@
|
|||||||
<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>
|
|
@ -1,46 +0,0 @@
|
|||||||
<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>
|
|
@ -1,23 +0,0 @@
|
|||||||
<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>
|
|
@ -1,61 +0,0 @@
|
|||||||
<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>
|
|
@ -1,41 +0,0 @@
|
|||||||
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'],
|
|
||||||
]);
|
|
@ -1,41 +0,0 @@
|
|||||||
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 Middleware = (context: Context) => void;
|
type RunMiddleware = (context: Context) => void;
|
||||||
function nextFactory(
|
function nextFactory(
|
||||||
context: Context,
|
context: Context,
|
||||||
middlewares: Array<Middleware>,
|
middlewares: Array<RunMiddleware>,
|
||||||
index: number
|
index: number
|
||||||
) {
|
) {
|
||||||
const subsequentMiddleware = middlewares[index];
|
const subsequentMiddleware = middlewares[index];
|
||||||
if (!subsequentMiddleware) return context.next;
|
if (!subsequentMiddleware) return context.next;
|
||||||
|
|
||||||
return (...args: Array<any>) => {
|
return (...args: any[]) => {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
context.next(...args as Array<any>);
|
context.next(...args);
|
||||||
subsequentMiddleware({
|
subsequentMiddleware({
|
||||||
...context,
|
...context,
|
||||||
next: nextFactory(context, middlewares, index++),
|
next: nextFactory(context, middlewares, index++),
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
export function runMiddleware(context: Context): void {
|
export function runMiddleware(context: Context) {
|
||||||
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<Middleware>),
|
: [to.meta.middleware]) as Array<RunMiddleware>),
|
||||||
];
|
];
|
||||||
|
|
||||||
middlewares[0]({ ...context, next: nextFactory(context, middlewares, 1) });
|
middlewares[0]({ ...context, next: nextFactory(context, middlewares, 1) });
|
||||||
|
@ -1,16 +0,0 @@
|
|||||||
// 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,13 +1,7 @@
|
|||||||
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';
|
|
||||||
|
|
||||||
export const themeConfig = new ThemeConfig();
|
createApp(App).use(store).use(router).mount("#app");
|
||||||
themeConfig.initTheme();
|
|
||||||
|
|
||||||
createApp(App).use(store).use(router).mount('#app');
|
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
import { useWebdavStore } from '@/store/webdav';
|
import { useWebdavStorage } from '@/store/webdav';
|
||||||
import { Context } from '@/middleware/Context';
|
import { Context } from '@/middleware/Context';
|
||||||
|
|
||||||
export const auth = ({ next }: Context) => {
|
export default function auth({ next, router }: Context) {
|
||||||
if (!useWebdavStore().currentSession?.isActive)
|
console.log('auth');
|
||||||
return next({ name: 'Login' });
|
if (!useWebdavStorage().currentSession?.isActive) return router.push({ name: 'Login' });
|
||||||
return next();
|
return next();
|
||||||
};
|
}
|
||||||
|
@ -1,9 +0,0 @@
|
|||||||
export type File = {
|
|
||||||
basename: string;
|
|
||||||
etag: string;
|
|
||||||
filename: string;
|
|
||||||
lastmod: string;
|
|
||||||
mime: string;
|
|
||||||
size: number;
|
|
||||||
type: string;
|
|
||||||
};
|
|
@ -1,6 +1,7 @@
|
|||||||
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> = [
|
||||||
@ -21,6 +22,9 @@ 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],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
13
src/store/auth.ts
Normal file
13
src/store/auth.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import {defineStore} from "pinia";
|
||||||
|
|
||||||
|
export const useAuthStorage = defineStore('auth', {
|
||||||
|
state: () => ({
|
||||||
|
user: '',
|
||||||
|
password: ''
|
||||||
|
}),
|
||||||
|
actions: {
|
||||||
|
login(){
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
@ -11,29 +11,31 @@ export type Session = {
|
|||||||
isActive: boolean;
|
isActive: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useWebdavStore = defineStore('auth', {
|
export const useWebdavStorage = defineStore('auth', {
|
||||||
state: () => ({
|
state: () => ({
|
||||||
sessions: [] as Array<Session>,
|
sessions: [] as Array<Session>,
|
||||||
currentSession: null as null | Session,
|
currentSession: null as null | Session,
|
||||||
}),
|
}),
|
||||||
actions: {
|
actions: {
|
||||||
async login({ user, pass }: Auth): Promise<Session> {
|
login({ user, pass }: Auth): Promise<Session> {
|
||||||
try {
|
return new Promise((resolve, reject) => {
|
||||||
const client = createClient(
|
try {
|
||||||
`${process.env.VUE_APP_ROOT_WEBDAV ?? ''}/api/dav/files/` as string,
|
const client = createClient(
|
||||||
{
|
process.env.VUE_APP_ROOT_WEBDAV as string,
|
||||||
authType: AuthType.Password,
|
{
|
||||||
username: user as string,
|
authType: AuthType.Digest,
|
||||||
password: pass as string,
|
username: user as string,
|
||||||
}
|
password: pass as string,
|
||||||
) as WebDAVClient;
|
}
|
||||||
const session = { client, isActive: true } as Session;
|
) as WebDAVClient;
|
||||||
this.sessions.push(session);
|
const session = { client, isActive: true } as Session;
|
||||||
this.currentSession = session;
|
this.sessions.push(session);
|
||||||
return session;
|
this.currentSession = session;
|
||||||
} catch (e) {
|
resolve(session);
|
||||||
throw 'login failed';
|
} catch (e) {
|
||||||
}
|
reject(e);
|
||||||
|
}
|
||||||
|
});
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
@ -1,20 +1,11 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="row mx-1 justify-content-center">
|
<h1>Files</h1>
|
||||||
<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 setup lang="ts">
|
<script>
|
||||||
import FileList from '@/components/files/FileList.vue';
|
export default {
|
||||||
import SideMenu from '@/components/SideMenu.vue';
|
name: 'Files',
|
||||||
import { useWebdavStore } from '@/store/webdav';
|
};
|
||||||
|
|
||||||
const store = useWebdavStore();
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style scoped></style>
|
||||||
|
@ -2,32 +2,22 @@
|
|||||||
<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 class="btn btn-primary" @click="login">Login</button><br />
|
<button @click="login">Login</button>
|
||||||
<span class="error">{{ error }}</span>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref } from 'vue';
|
import { ref } from 'vue';
|
||||||
import { useWebdavStore } from '@/store/webdav';
|
import { useWebdavStorage } 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 () => {
|
||||||
try {
|
await useWebdavStorage()
|
||||||
await store.login({ user: user.value, pass: pass.value });
|
.login({ user, pass })
|
||||||
error.value = '';
|
.then(async session => {
|
||||||
await router.push({ name: 'Files' });
|
console.log(await session.client.getDirectoryContents('/'));
|
||||||
} 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