Merge commit '9790ef4dacdf729d8825dbe745379bd6c669b9dd' as 'components/rfs'

This commit is contained in:
2025-08-16 21:12:45 +02:00
96 changed files with 14003 additions and 0 deletions

View File

@@ -0,0 +1 @@
VITE_API_URL="http://localhost:4000"

24
components/rfs/frontend/.gitignore vendored Normal file
View File

@@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

View File

@@ -0,0 +1,3 @@
{
"recommendations": ["Vue.volar"]
}

View File

@@ -0,0 +1,13 @@
# build stage
FROM node:lts-alpine as build-stage
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build
# production stage
FROM nginx:stable-alpine as production-stage
COPY --from=build-stage /app/dist /usr/share/nginx/html
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

View File

@@ -0,0 +1,82 @@
# Threefold RFS
## Description
`Threefold RFS` is a frontend that helps manage the RFS server for creating, mounting, and extracting FungiStore lists, or fl for short. An fl is a simple format that stores information about a whole filesystem in a compact way. It doesn't hold the actual data but includes enough details to retrieve the data from a store.
## Prerequesites
- build essentials
```bash
sudo apt-get install build-essential
```
- [node js](https://nodejs.org/en/download/package-manager)
- [rust](https://www.rust-lang.org/tools/install)
- Cargo, to be configured to run in the shell
- musl tool
```bash
sudo apt install musl-tools
```
## Installation
```bash
git clone https://github.com/threefoldtech/rfs.git
```
### backend
In fl-server dir:
- create flists dir containaing dirs for each user
ex:
- fl-server
- flists
- user1
- user2
- include config file
ex:
```yml
host='localhost'
port=4000
store_url=['dir:///tmp/store0']
flist_dir='flists'
jwt_secret='secret'
jwt_expire_hours=5
[[users]] # list of authorized user in the server
username = "user1"
password = "password1"
[[users]]
username = "user2"
password = "password2"
```
- Move to `fl-server` directory and execute the following command to run the backend:
```bash
cargo run --bin fl-server -- --config-path config.toml
```
### frontend
- Move to `frontend` directory, open new terminal and execute the following commands to run the frontend:
```bash
npm install
npm run dev
```
## Usage
- Login with users listed in config.toml with their username and password
- Create Flist
- Preview Flist
- List all Flists
- Download Flist

View File

@@ -0,0 +1,18 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="icon" type="image" href="./src/assets/logo.png">
<title>Threefold Flist</title>
<link href="./src/style.css" rel="stylesheet">
<link href="https://cdn.materialdesignicons.com/5.4.55/css/materialdesignicons.min.css" rel="stylesheet">
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

1476
components/rfs/frontend/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,29 @@
{
"name": "frontend",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vue-tsc -b && vite build",
"preview": "vite preview"
},
"dependencies": {
"@mdi/font": "^7.4.47",
"@vueuse/core": "^10.11.1",
"axios": "^1.7.3",
"filesize": "^10.1.4",
"mdi": "^2.2.43",
"vue": "^3.4.31",
"vue-router": "^4.4.2",
"vue3-toastify": "^0.2.2",
"vuetify": "^3.6.14"
},
"devDependencies": {
"@types/node": "^22.1.0",
"@vitejs/plugin-vue": "^5.0.5",
"typescript": "^5.2.2",
"vite": "^5.3.4",
"vue-tsc": "^2.0.24"
}
}

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@@ -0,0 +1,33 @@
<template>
<v-app>
<router-view v-slot="{ Component, route }">
<Navbar v-if="route.path != `/login`"></Navbar>
<v-main class="mn-height" style="--v-layout-left: 0px;" >
<div :key="route.path">
<component :is="Component" />
</div>
</v-main>
<Footer v-if="route.path != `/login`"></Footer>
</router-view>
</v-app>
</template>
<script setup lang="ts">
import Footer from './components/Footer.vue';
import Navbar from './components/Navbar.vue';
</script>
<style scoped>
.logo {
height: 6em;
padding: 1.5em;
will-change: filter;
transition: filter 300ms;
}
.logo:hover {
filter: drop-shadow(0 0 2em #646cffaa);
}
.logo.vue:hover {
filter: drop-shadow(0 0 2em #42b883aa);
}
</style>

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 336 KiB

View File

@@ -0,0 +1,11 @@
import axios from "axios";
export const api = axios.create({
baseURL: import.meta.env.VITE_API_URL,
headers: {
"Content-Type": "application/json",
Authorization: "Bearer " + sessionStorage.getItem("token"),
},
});

View File

@@ -0,0 +1,308 @@
<template>
<div class="d-flex flex-column justify-center mt-10" >
<v-container fluid>
<v-row justify="center">
<v-col cols="8">
<h2 class="mb-2">Create a Flist:</h2>
</v-col>
</v-row>
<v-form>
<v-row justify="center">
<v-col cols="8">
<label
for="image-name"
class="text-subtitle-1 text-medium-emphasis d-flex align-center"
>
Image Name<span style="color: red">*</span>
</label>
<v-text-field
class="pr-5 rounded"
id="image-name"
v-model="flist.image_name"
variant="solo-filled"
density="compact"
required
placeholder="example: redis, keinos/sqlite3, alpine"
>
</v-text-field>
<v-checkbox
value="true"
v-model="privateReg"
hide-details
density="compact"
><template v-slot:label>
<span class="text-subtitle-2">Private Registery</span>
<v-tooltip activator="parent" location="start">Check this box to pull the Docker image from your private registry instead of the public repository.</v-tooltip>
</template>
</v-checkbox>
<div v-if="privateReg">
<v-alert text="Select a sign-in method" type="info" density="compact" color = "#1aa18f" closable width="60em"></v-alert>
<v-radio-group class="p-0 m-0" v-model="privateType" inline>
<v-radio value="username">
<template v-slot:label>
<span class="text-subtitle-2">Username - Password</span>
</template>
</v-radio>
<v-radio value="email">
<template v-slot:label>
<span class="text-subtitle-2">Email - Password</span>
</template>
</v-radio>
<v-radio value="token">
<template v-slot:label>
<span class="text-subtitle-2">Identity Token</span>
<v-tooltip activator="parent" location="bottom">Token you can as an alternative to your email/username password</v-tooltip>
</template>
</v-radio>
</v-radio-group>
<v-container class="pr-0 pl-0">
<v-row>
<v-col>
<div v-if="privateType === `email`">
<label
for="email"
class="text-subtitle-1 text-medium-emphasis d-flex align-center justify-space-between"
>
Email
</label>
<v-text-field
class="pr-5 rounded"
id="email"
v-model="flist.email"
variant="solo-filled"
density="compact"
placeholder="johndoe@gmail.com"
type="email"
>
</v-text-field>
</div>
<div v-if="privateType !== `email`">
<label
for="username"
class="text-subtitle-1 text-medium-emphasis d-flex align-center justify-space-between"
>
Username
</label>
<v-text-field
class="pr-5 text-medium-emphasis"
id="username"
v-model="flist.username"
variant="solo-filled"
density="compact"
:placeholder="
privateType === `token` ? `token` : `johndoe`
"
:value="privateType === `token`?`token`:``"
:readonly="privateType === `token`"
>
</v-text-field>
</div>
</v-col>
<v-col>
<div
v-if="privateType.length != 0 && privateType !== `token`"
>
<label
for="password"
class="text-subtitle-1 text-medium-emphasis d-flex align-center justify-space-between"
>
Password
</label>
<v-text-field
class="pr-5 rounded"
id="password"
v-model="flist.password"
variant="solo-filled"
:append-inner-icon="visible ? 'mdi-eye-off' : 'mdi-eye'"
:type="visible ? 'text' : 'password'"
@click:append-inner="visible = !visible"
density="compact"
>
</v-text-field>
</div>
<div v-if="privateType === `token`">
<label
for="identity-token"
class="text-subtitle-1 text-medium-emphasis d-flex align-center justify-space-between"
>
Identity Token
</label>
<v-text-field
class="pr-5 rounded"
id="identity-token"
v-model="flist.identity_token"
variant="solo-filled"
density="compact"
>
</v-text-field>
</div>
</v-col>
</v-row>
</v-container>
</div>
<v-checkbox
value="true"
v-model="registeryAddress"
hide-details
density="compact"
><template v-slot:label>
<span class="text-subtitle-2">Self Hosted Registery</span>
<v-tooltip activator="parent" location="start">Check this box to pull the Docker image from your self-hosted registry using registery address</v-tooltip>
</template>
</v-checkbox>
<div v-if="registeryAddress">
<label
for="server-address"
class="text-subtitle-1 text-medium-emphasis d-flex align-center justify-space-between"
>
Registery Address
</label>
<v-text-field
class="pr-5 rounded"
id="server-address"
v-model="flist.server_address"
variant="solo-filled"
density="compact"
placeholder="localhost:5000"
>
</v-text-field>
</div>
<v-checkbox
value="true"
v-model="registeryToken"
density="compact"
hide-details
><template v-slot:label>
<span class="text-subtitle-2">Web Registery Token</span>
<v-tooltip activator="parent" location="start">Check this box to use web registry token to pull image from your registry with secure authentication</v-tooltip>
</template>
</v-checkbox>
<div v-if="registeryToken">
<label
for="registery-token"
class="text-subtitle-1 text-medium-emphasis d-flex align-center justify-space-between"
>
Registery Token
</label>
<v-text-field
class="pr-5 rounded mb-5"
id="registery-token"
v-model="flist.registry_token"
variant="solo-filled"
density="compact"
>
</v-text-field>
</div>
</v-col>
</v-row>
<v-row>
<v-col offset="8" class="pa-0">
<div class="position-relative" style="left: -5%;" >
<v-btn
class="pr-5 rounded-pill background-green mb-8 mt-5 text-white"
size="large"
width="50%"
@click="create"
:disabled="pending"
v-if = "!pending"
>
Create
</v-btn>
<v-progress-linear
:size="70"
color="#1aa18f"
indeterminate
class="mb-5 mt-5 w-50"
rounded=""
height="20"
v-else
>
<template v-slot:default> {{ progress }} % </template>
</v-progress-linear>
</div>
</v-col>
</v-row>
</v-form>
</v-container>
</div>
</template>
<script setup lang="ts">
import { ref, watch } from "vue";
import { Flist } from "../types/Flist";
import { toast } from "vue3-toastify";
import "vue3-toastify/dist/index.css";
import { api } from "../client";
import router from "../router";
const pending = ref<boolean>(false);
let progress = ref<number>(0);
const stopPolling = ref<boolean>(false);
let polling: NodeJS.Timeout;
let id = ""
const pullLists = async () => {
try {
const response = await api.get("v1/api/fl/" + id);
if (response.data.flist_state.InProgress) {
progress.value = Math.floor(
response.data.flist_state.InProgress.progress
);
} else {
stopPolling.value = true;
pending.value = false;
router.push({name: "myflists"})
}
} catch (error: any) {
pending.value = false;
stopPolling.value = true;
toast.error(error.response?.data)
}
};
watch(stopPolling, () => {
if (stopPolling.value) {
clearInterval(polling);
}
});
const privateReg = ref<boolean>(false);
const registeryAddress = ref<boolean>(false);
const registeryToken = ref<boolean>(false);
const privateType = ref<string>("username");
const flist = ref<Flist>({
auth: "",
email: "",
identity_token: "",
image_name: "",
password: "",
registry_token: "",
server_address: "",
username: "",
});
const visible = ref<boolean>(false);
const create = async () => {
try {
const response = await api.post("/v1/api/fl", flist.value);
id = response.data.id
pending.value = true
polling = setInterval(pullLists, 1 * 10000);
} catch (error: any) {
toast.error(error.response?.data || "error occured");
const errors: Number[] = [401, 403];
if (errors.includes(error.response?.status)) {
sessionStorage.removeItem("token");
}
}
};
</script>

View File

@@ -0,0 +1,23 @@
<template>
<v-footer class="bg-grey-darken-3 d-flex justify-center w-100 m-0">
All rights reserved © 2024 -
<a
href="https://threefold.io"
style="color: inherit; text-decoration: none"
>
ThreeFold <v-icon icon="mdi-link" style="font-size: 1em" />
</a>
<a
href="https://github.com/threefoldtech"
style="color: inherit; text-decoration: none"
>
<v-icon icon="mdi-github" style="margin-left: 7px" />
</a>
</v-footer>
</template>
<style>
.v-footer {
height: 7% !important;
}
</style>

View File

@@ -0,0 +1,158 @@
<template >
<div class="w-100 position-relative" style="top: -62.5px" >
<v-img :src="image" cover style="z-index: 2"></v-img>
</div>
<div class="d-flex justify-center mt-0">
<v-navigation-drawer
app
class="position-absolute mx-height"
style="top: 30%; left: 0; height: 62.5%; width: fit-content; min-width: 12.5%;"
>
<v-list>
<v-list-item nav>
<v-list-item-title class=" text-h6 " > Users</v-list-item-title>
</v-list-item>
<v-divider></v-divider>
<v-list-item density="compact"
v-for="userName in userNameList"
:key="userName"
@click="username = userName"
>
<template v-slot:prepend >
<v-icon icon="mdi-account" color="#1aa18f" style="font-size: 15px;"></v-icon>
<v-list-item-title style="padding: 2px 4px;
font-size: 15px;
font-weight: 300;">
{{ userName }}
</v-list-item-title>
</template>
</v-list-item>
</v-list>
</v-navigation-drawer>
<v-container
class="d-flex flex-column w-75 "
fluid
style="height: fit-content; position: relative; left: 6%;"
>
<h2 class="mb-2" v-if="username.length != 0">
<v-icon icon="mdi-account" color="#1aa18f"></v-icon>{{ username }}
</h2>
<!-- table containe flists -->
<v-data-table density="compact"
:items="filteredFlist"
:headers="tableHeader"
dense
class="thick-border "
items-per-page="25"
>
<template #item.name="{ value }">
<v-icon icon="mdi-text-box" class="mr-1" color="grey"/>
<span class="file-name">{{ value }}</span>
</template>
<template v-slot:item.preview = "{index}" >
<a :href="`/` + filteredFlist[index].path_uri">
<v-btn class="elevation-0">
<v-icon icon="mdi-eye-outline" color="grey"></v-icon>
</v-btn>
</a>
</template>
<template #item.size="{value}">
{{filesize(value, {standard: "jedec", precision: 3})}}
</template>
<template #item.last_modified="{ value }">
{{ new Date(value * 1000).toString().split("(")[0] }}
</template>
<template #item.path_uri="{ value }">
<v-btn class="elevation-0">
<a :href="baseURL + `/` + value" download>
<v-icon icon="mdi-download" color="grey"></v-icon
></a>
<v-tooltip activator="parent" location="start"
>Download flist</v-tooltip
>
</v-btn>
<v-btn @click="copyLink(baseURL + `/` + value)" class="elevation-0">
<v-icon icon="mdi-content-copy" color="grey"></v-icon>
<v-tooltip activator="parent">Copy Link</v-tooltip>
</v-btn>
</template>
</v-data-table>
</v-container>
</div>
</template>
<script setup lang="ts">
import { onMounted, ref, watch } from "vue";
import image from "../assets/home.png";
import { FlistsResponseInterface, FlistBody } from "../types/Flist.ts";
import { toast } from "vue3-toastify";
import "vue3-toastify/dist/index.css";
import { api } from "../client.ts";
import { copyLink } from "../helpers.ts";
import {filesize} from "filesize";
const baseURL = import.meta.env.VITE_API_URL;
const tableHeader = [
{ title: "File Name", key: "name" },
{ title: "Preview", key:"preview"},
{ title: "Size", key: "size" },
{ title: "Last Modified", key: "last_modified" },
{ title: "Download", key: "path_uri", sortable: false },
];
var flists = ref<FlistsResponseInterface>({});
const username = ref("");
const userNameList = ref<string[]>([]);
let filteredFlist = ref<FlistBody[]>([]);
const filteredFlistFn = () => {
filteredFlist.value = [];
const map = flists.value;
if (username.value.length === 0) {
for (var flistMap in map) {
for (let flist of map[flistMap]) {
if (flist.progress === 100) {
filteredFlist.value.push(flist);
}
}
}
} else {
for (let flist of map[username.value]) {
if (flist.progress === 100) {
filteredFlist.value.push(flist);
}
}
}
};
const getUserNames = () => {
const list: string[] = [];
const map = flists.value;
for (var flistMap in map) {
list.push(flistMap);
}
userNameList.value = list;
};
onMounted(async () => {
try {
flists.value = (await api.get<FlistsResponseInterface>("/v1/api/fl")).data;
getUserNames();
filteredFlistFn();
} catch (error: any) {
toast.error(error.response?.data);
}
});
watch(username, () => {
filteredFlistFn();
});
</script>
<style lang="css" scoped>
.mx-height {
max-height: 600px;
}
.mn-height {
min-height: calc(100% - 37%);
}
</style>

View File

@@ -0,0 +1,113 @@
<template>
<v-container fluid class="overflow-hidden pa-0" style="height: 100vh;">
<v-row class="h-100 ma-0 pa-0">
<v-col :cols="4" class="position-relative ma-0 pa-0 h-100">
<v-img :src="image" cover height="100%" style="z-index: 900"> </v-img>
<v-container
class="position-absolute top-0 d-flex flex-column justify-center ga-0"
style="z-index: 1000; height: 70%"
>
<v-img
:src="whiteLogo"
height="10%"
width="15%"
class="mb-5 flex-grow-0"
></v-img>
<p class="mt-0 text-white" style="width: 90%">
FungiStore is the main tool to create, mount, and extract FungiStore lists (Fungilist or FL for short). An FL is a simple format used to store information about an entire filesystem in a compact form. It does not contain the data itself but provides enough information to retrieve this data from a store.
</p>
</v-container>
</v-col>
<v-col :cols="8" class="d-flex align-center">
<v-container class="d-flex flex-column align-center justify-center">
<v-col :cols="6">
<v-form>
<v-img :src="logo" class="mb-10" height="10%" width="15%"></v-img>
<h2 class="mb-5">Sign in</h2>
<label
for="username"
class="text-subtitle-1 text-medium-emphasis d-flex align-center justify-space-between"
>
Username
</label>
<v-text-field
class="pr-5 rounded"
v-model="user.username"
variant="outlined"
density="compact"
id="username"
required
>
</v-text-field>
<label
for="password"
class="text-subtitle-1 text-medium-emphasis d-flex align-center justify-space-between"
>
Password
</label>
<v-text-field
class="mb-5 pr-5 rounded"
v-model="user.password"
:append-inner-icon="visible ? 'mdi-eye-off' : 'mdi-eye'"
:type="visible ? 'text' : 'password'"
variant="outlined"
@click:append-inner="visible = !visible"
density="compact"
id="password"
required
>
</v-text-field>
<v-btn
class="pr-5 rounded-pill background-green text-white position-relative"
style="left: 205px;"
size="large"
width="50%"
:disabled="loading"
@click="login"
>Sign In</v-btn>
</v-form>
</v-col>
</v-container>
</v-col>
</v-row>
</v-container>
</template>
<script setup lang="ts">
import { ref } from "vue";
import image from "./../assets/side.png";
import logo from "./../assets/logo.png";
import whiteLogo from "../assets/logo_white.png";
import { User } from "../types/User.ts";
import { api } from "../client.ts";
import { toast } from "vue3-toastify";
import "vue3-toastify/dist/index.css";
import router from "../router/index.ts";
const user = ref<User>({ username: "", password: "" });
const loading = ref<boolean>(false)
const visible = ref<boolean>(false);
const login = async () => {
try {
const response = await api.post("/v1/api/signin", user.value);
const token = response.data.access_token;
sessionStorage.setItem("token", token);
sessionStorage.setItem("username", user.value.username);
api.interceptors.request.use((config) => {
if (token) {
config.headers["Authorization"] = `Bearer ${token}`;
}
return config;
});
router.push("/myflists")
} catch (error: any) {
toast.error(error.response?.data || "error occured");
}
};
</script>

View File

@@ -0,0 +1,63 @@
<template>
<v-app-bar color="#1aa18f">
<v-app-bar-nav-icon to="/" class="ml-8">
<v-img :src="whiteLogo" contain height="50px" width="50px"></v-img>
</v-app-bar-nav-icon>
<v-spacer> </v-spacer>
<div class="mr-5" v-if="auth === null || auth?.length === 0">
<v-btn to="login">Login</v-btn>
</div>
<div class="mr-5" v-else>
<v-btn to="create"
><v-icon icon="mdi-plus-circle-outline" class="mr-2"></v-icon>Create
flist</v-btn
>
<v-menu class="white">
<template v-slot:activator="{ props }">
<v-btn
class="align-self-center me-4"
height="100%"
rounded="50%"
variant="plain"
v-bind="props"
style="font-size: 20px"
>
<v-icon icon="mdi-account"></v-icon>
</v-btn>
</template>
<v-list>
<v-list-item>
<v-btn><a href="/myflists" class="text-black" style="text-decoration:none;">My FLists</a></v-btn>
</v-list-item>
<v-list-item>
<v-btn @click="logout"
><v-icon icon="mdi-logout" style="font-size: 20px" />log
out</v-btn
>
</v-list-item>
</v-list>
</v-menu>
</div>
</v-app-bar>
</template>
<script setup lang="ts">
import { ref } from "vue";
import whiteLogo from "../assets/logo_white.png";
import { toast } from "vue3-toastify";
import router from "../router";
const auth= ref<string|null>(sessionStorage.getItem("token"));
const logout = async () => {
try {
sessionStorage.removeItem("token")
sessionStorage.removeItem("username")
auth.value = sessionStorage.getItem("token");
router.push("/")
} catch (error: any) {
toast.error(error.response?.data || "error occured");
}
};
</script>

View File

@@ -0,0 +1,140 @@
<template>
<div class="w-100 position-relative" style="top: -62.5px">
<v-img :src="image" cover style="z-index: 2"></v-img>
<div
class="position-absolute w-100 text-white d-flex justify-content align-content "
style="z-index: 4; top: 55%;left:40%;"
>
</div>
</div>
<div class="mn-height mb-10" v-if="!pending">
<v-container class="m-0 pa-0">
<v-row>
<div>
<h2 class="text-h4 mb-3">{{
id
}}</h2>
<p>This Flist was created by <v-chip color="#1aa18f" label>{{ username }} </v-chip> </p>
</div>
</v-row>
<v-row class="d-flex flex-column">
<h3 class="text-subtitle-1 text-grey-darken-2">Source file</h3>
<v-text-field rounded="20" variant="outlined" density="compact" readonly class="text-grey-darken-1 mr-0">
{{ baseURL + url }}
<template #append>
<v-btn
color="#1aa18f"
value="Copy"
class="Btn"
@click="copyLink(baseURL + url)">
Copy
</v-btn>
</template>
</v-text-field>
</v-row>
<v-row class="d-flex flex-column">
<h3 class="text-subtitle-1 text-grey-darken-2">Archive Checksum (MD5)</h3>
<v-text-field rounded="20" variant="outlined" density="compact" readonly class="text-grey-darken-1 mr-0">
{{flistPreview.checksum}}
<template #append>
<v-btn
color="#1aa18f"
value="Copy"
class="Btn"
@click="copyLink(flistPreview.checksum)">
Copy
</v-btn>
</template>
</v-text-field>
</v-row>
<v-row class="d-flex flex-column">
<h3 class="text-subtitle-1 text-grey-darken-2">Metadata</h3>
<v-text-field rounded="20" variant="outlined" density="compact" readonly class="text-grey-darken-1 mr-0" width="98.5%">
{{ flistPreview.metadata}}
<template #prepend-inner>
<v-chip color="#1aa18f" label class ="chip">Backend (default)</v-chip>
</template>
</v-text-field>
</v-row>
<v-row class="d-flex flex-column">
<h3 class="text-subtitle-1 text-grey-darken-2">Content</h3>
<v-textarea :model-value="showContent" variant="outlined" readonly rows="1" :class= "linkDecoration" class="text-grey-darken-1" auto-grow width="98.5%" @click="contentShow()">
</v-textarea>
</v-row>
</v-container>
</div>
<div class="d-flex align-center justify-center mb-12 mt-12" v-else>
<v-progress-circular
:size="70"
:width="7"
color="#1aa18f"
indeterminate
class="mb-5"
>
</v-progress-circular>
</div>
</template>
<script setup lang="ts">
import { onMounted, ref } from "vue";
import image from "../assets/home.png";
import { toast } from "vue3-toastify";
import "vue3-toastify/dist/index.css";
import { api } from "../client.ts";
import { copyLink } from "../helpers.ts";
import { FlistPreview } from "../types/Flist.ts";
const pending = ref<boolean>(true)
const flistPreview = ref<FlistPreview>({checksum:"", content:[], metadata:""});
const urlPartition = window.location.href.split("/")
const id = ref<string>(urlPartition[urlPartition.length - 1])
const username = ref<string>(urlPartition[urlPartition.length - 2])
const baseURL = ref<string>(import.meta.env.VITE_API_URL + "/");
const url ="flists" + "/" + username.value + "/" + id.value
const showContent = ref<string>()
const linkDecoration = ref<string>("text-as-anchor")
const contentShow = () => {
showContent.value = flistPreview.value?.content.join("\n")
linkDecoration.value = ""
}
onMounted(async () => {
try {
const encodedUrl = url.replaceAll("/", "%2F");
flistPreview.value = (await api.get<FlistPreview>("/v1/api/fl/preview/" + encodedUrl)).data;
flistPreview.value.content = flistPreview.value.content.slice(1)
showContent.value = "show content on click"
pending.value = false
} catch (error: any) {
toast.error(error.response?.data);
}
});
</script>
<style scoped>
.Btn{
position: relative;
left: -18px;
height: 40px;
width: 110px;
margin-left:0px;
}
.chip{
height: 40px;
position: relative;
left: -11px;
}
.text-as-anchor {
color: #42A5F5;
cursor: pointer;
}
.text-as-anchor:hover {
text-decoration: underline;
}
</style>

View File

@@ -0,0 +1,129 @@
<template>
<div>
<v-container class="pa-0">
<v-row no-gutters class="pa-0 ma-0">
<div class="user">
<h2 class="mt-5 mb-5 text-h5 text-grey-darken-2">
<v-icon icon="mdi-account" color="#1aa18f"></v-icon
>{{ loggedInUser }}
</h2>
</div>
</v-row>
<v-row no-gutters class="pa-0 ma-0">
<v-data-table
density="compact"
v-if="loggedInUser"
:items="currentUserFlists"
:headers="tableHeader"
dense
items-per-page="25"
class="thick-border"
>
<template #item.name="{ value }">
<v-icon icon="mdi-text-box" class="mr-1" color="grey" />
<span class="file-name">{{ value }}</span>
</template>
<template v-slot:item.preview = "{index}" >
<a :href="`/` + currentUserFlists[index].path_uri">
<v-btn class="elevation-0">
<v-icon icon="mdi-eye-outline" color="grey"></v-icon>
</v-btn>
</a>
</template>
<template #item.size="{ value }">
{{ filesize(value, { standard: "jedec", precision: 3 }) }}
</template>
<template v-slot:item.path_uri="{ index, value }">
<template v-if="currentUserFlists[index].progress === 100">
<v-btn class="elevation-0">
<a :href="baseURL + `/` + value" download>
<v-icon icon="mdi-download" color="grey"></v-icon
></a>
<v-tooltip activator="parent" location="start"
>Download flist</v-tooltip
>
</v-btn>
<v-btn
@click="copyLink(baseURL + `/` + value)"
class="elevation-0"
>
<v-icon icon="mdi-content-copy" color="grey"></v-icon>
<v-tooltip activator="parent">Copy Link</v-tooltip>
</v-btn>
</template>
<template v-else>
<span>loading... </span>
</template>
</template>
<template #item.last_modified="{ value }">
{{ new Date(value * 1000).toString().split("(")[0] }}
</template>
<template v-slot:item.progress="{ value }" class="w-25">
<template v-if="value != 100">
<v-progress-linear
:model-value="value"
color="#1aa18f"
height="20"
rounded="sm"
>
<template v-slot:default="{ value }">
<span class="text-white">{{ Math.floor(value) }}%</span>
</template>
</v-progress-linear>
</template>
<template v-else>
<v-chip color="#1aa18f">finished</v-chip>
</template>
</template>
</v-data-table>
</v-row>
</v-container>
</div>
</template>
<script setup lang="ts">
import { FlistsResponseInterface } from "../types/Flist.ts";
import { computed } from "vue";
import { onMounted, ref } from "vue";
import { toast } from "vue3-toastify";
import { api } from "../client.ts";
import { copyLink } from "../helpers.ts";
import { filesize } from "filesize";
const tableHeader = [
{ title: "File Name", key: "name" },
{ title: "Preview", key:"preview"},
{ title: "Size", key: "size" },
{ title: "Last Modified", key: "last_modified" },
{ title: "Download", key: "path_uri", sortable: false },
{ title: "Progress", key: "progress", width: "20%" },
];
const loggedInUser = sessionStorage.getItem("username");
var flists = ref<FlistsResponseInterface>({});
const baseURL = import.meta.env.VITE_API_URL;
let currentUserFlists = computed(() => {
return loggedInUser?.length ? flists.value[loggedInUser] : [];
});
onMounted(async () => {
try {
flists.value = (await api.get<FlistsResponseInterface>("/v1/api/fl")).data;
currentUserFlists = computed(() => {
return loggedInUser?.length ? flists.value[loggedInUser] : [];
});
} catch (error: any) {
toast.error(error.response?.data);
}
});
</script>
<style scoped>
.user .v-icon--size-default {
font-size: 25px;
}
</style>

View File

@@ -0,0 +1,8 @@
import { useClipboard } from "@vueuse/core";
import { toast } from "vue3-toastify";
const { copy } = useClipboard();
export const copyLink = (url: string) => {
copy(url);
toast.success("Link Copied to Clipboard");
};

View File

@@ -0,0 +1,17 @@
import { createApp } from "vue";
import "vuetify/styles";
import { createVuetify } from "vuetify";
import * as components from "vuetify/components";
import * as directives from "vuetify/directives";
import App from "./App.vue";
import router from "./router/index";
import createToast from "vue3-toastify";
const toast = createToast;
const vuetify = createVuetify({
components,
directives,
});
createApp(App).use(router).use(toast).use(vuetify).mount("#app");

View File

@@ -0,0 +1,52 @@
import { createRouter, createWebHistory, RouteRecordRaw } from "vue-router";
const Login = () => import("../components/Login.vue");
const CreateFlist = () => import("../components/CreateFlist.vue");
const Home = () => import("../components/Home.vue");
const UserFlist = () => import("../components/UserFlist.vue");
const PreviewFlist = () => import("../components/PreviewFlist.vue");
const routes: Array<RouteRecordRaw> = [
{
path: "/login",
name: "login",
component: Login,
},
{
path: "/myflists",
name: "myflists",
component: UserFlist,
meta: { requiresAuth: true },
},
{
path: "/create",
name: "create",
component: CreateFlist,
meta: { requiresAuth: true },
},
{
path: "/flists/:username/:id",
name: "previewflist",
component: PreviewFlist,
},
{
path: "/",
name: "home",
component: Home,
},
];
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes,
});
router.beforeEach((to, _, next) => {
const token: string | null = sessionStorage.getItem("token");
if (to.meta.requiresAuth && (token == null || token.length == 0)) {
next({ name: "login" });
} else {
next();
}
});
export default router;

View File

@@ -0,0 +1,21 @@
.background-green {
background-color: #1aa18f !important;
}
.thick-border .v-data-table__wrapper {
border: 3px solid #000;
}
.v-data-table-footer__items-per-page {
display: none !important;
}
.v-data-table td{
padding: 4px 8px;
font-size: 12px;
font-weight: 500;
}
.mn-height {
min-height: calc(100% - 7%);
}
.file-name {
font-weight: 500;
}

View File

@@ -0,0 +1,30 @@
export interface Flist {
auth: string;
email: string;
identity_token: string;
image_name: string;
password: string;
registry_token: string;
server_address: string;
username: string;
}
export interface FlistBody {
is_file: Boolean;
last_modified: bigint;
name: string;
path_uri: string;
progress: number;
size: number;
}
export interface FlistsResponseInterface {
[key: string]: FlistBody[];
}
export interface FlistPreview{
checksum: string;
content: string[];
metadata: string;
}

View File

@@ -0,0 +1,4 @@
export interface User {
username: string;
password: string;
}

View File

@@ -0,0 +1 @@
/// <reference types="vite/client" />

View File

@@ -0,0 +1,27 @@
{
"compilerOptions": {
"composite": true,
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
"target": "ES2021",
"useDefineForClassFields": true,
"module": "ESNext",
"lib": ["ES2021", "DOM", "DOM.Iterable"],
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"moduleDetection": "force",
"noEmit": true,
"jsx": "preserve",
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true
},
"include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"]
}

View File

@@ -0,0 +1,11 @@
{
"files": [],
"references": [
{
"path": "./tsconfig.app.json"
},
{
"path": "./tsconfig.node.json"
}
]
}

View File

@@ -0,0 +1,13 @@
{
"compilerOptions": {
"composite": true,
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
"skipLibCheck": true,
"module": "ESNext",
"moduleResolution": "bundler",
"allowSyntheticDefaultImports": true,
"strict": true,
"noEmit": true
},
"include": ["vite.config.ts"]
}

View File

@@ -0,0 +1,7 @@
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [vue()],
})