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