]> git.ipfire.org Git - pbs.git/commitdiff
frontend: Add the filelist to the package view
authorMichael Tremer <michael.tremer@ipfire.org>
Mon, 7 Jul 2025 17:09:29 +0000 (17:09 +0000)
committerMichael Tremer <michael.tremer@ipfire.org>
Mon, 7 Jul 2025 17:09:29 +0000 (17:09 +0000)
Signed-off-by: Michael Tremer <michael.tremer@ipfire.org>
frontend/src/api/packages.ts
frontend/src/components/PackageFilelist.vue [new file with mode: 0644]
frontend/src/icons.ts
frontend/src/utils/format.ts
frontend/src/views/PackageByUUIDView.vue

index be74d81529f58f585e9167e80524b2de49b10728..0c8f9b88fddf9c942f0e42411a4ff1e478a0db6d 100644 (file)
@@ -111,3 +111,9 @@ export async function fetchPackage(uuid: string): Promise<Package> {
        const response = await api.get(`/v1/packages/${uuid}`);
        return response.data;
 }
+
+// Fetch the package filelist
+export async function fetchPackageFilelist(uuid: string): Promise<File[]> {
+       const response = await api.get(`/v1/packages/${uuid}/filelist`);
+       return response.data;
+}
diff --git a/frontend/src/components/PackageFilelist.vue b/frontend/src/components/PackageFilelist.vue
new file mode 100644 (file)
index 0000000..934cbca
--- /dev/null
@@ -0,0 +1,110 @@
+<script setup lang="ts">
+       import { computed, ref, onMounted } from "vue";
+
+       // API
+       import type { Package, File } from "@/api/packages";
+       import { fetchPackageFilelist } from "@/api/packages";
+
+       // Components
+       import Icon from "@/components/Icon.vue";
+       import Loader from "@/components/Loader.vue";
+       import Notification from "@/components/Notification.vue";
+       import Section from "@/components/Section.vue";
+
+       // Utils
+       import { formatMode, formatSize } from "@/utils/format";
+
+       // Fetch the package
+       const { pkg } = defineProps<{
+               pkg: Package,
+       }>();
+
+       const filelist = ref<File[] | null>(null);
+       const loading = ref(true);
+       const error = ref<Error | null>(null);
+
+       onMounted(async () => {
+               try {
+                       filelist.value = await fetchPackageFilelist(pkg.uuid);
+               } catch (err) {
+                       error.value = err as Error;
+               } finally {
+                       loading.value = false;
+               }
+       });
+</script>
+
+<template>
+       <Section :title="$t('Filelist')">
+               <!-- Show a loading indicator if we are still loading -->
+               <Loader v-if="loading" />
+
+               <!-- Show an error message if there has been an error -->
+               <Notification v-else-if="error" is-danger>
+                       {{ error }}
+               </Notification>
+
+               <!-- Otherwise show the filelist -->
+               <table v-else class="table is-striped is-hoverable is-fullwidth">
+                       <tbody>
+                               <tr>
+                                       <!-- Mode & Ownership -->
+                                       <th class="is-narrow"></th>
+                                       <th class="is-narrow"></th>
+
+                                       <!-- Size -->
+                                       <th class="is-narrow">
+                                               {{ $t("Size") }}
+                                       </th>
+
+                                       <!-- Path -->
+                                       <th>
+                                               {{ $t("Path") }}
+                                       </th>
+
+                                       <!-- Actions -->
+                                       <th class="is-narrow"></th>
+                               </tr>
+                       </tbody>
+
+                       <tbody>
+                               <tr v-for="file in filelist" :key="file.path">
+                                       <!-- Mode -->
+                                       <td class="is-family-monospace has-text-right is-narrow">
+                                               {{ formatMode(file.mode) }}
+                                       </td>
+
+                                       <!-- Ownership -->
+                                       <td class="is-family-monospace has-text-centered is-narrow">
+                                               {{ file.uname.padStart(6) }}:{{ file.gname.padEnd(6) }}
+                                       </td>
+
+                                       <!-- Size -->
+                                       <td class="is-family-monospace has-text-right is-narrow">
+                                               <span v-if="file.size">
+                                                       {{ formatSize(file.size) }}
+                                               </span>
+                                               <span v-else>
+                                                       &dash;
+                                               </span>
+                                       </td>
+
+                                       <!-- Path -->
+                                       <td class="is-family-monospace">
+                                               {{ file.path }}
+                                       </td>
+
+                                       <!-- Actions -->
+                                       <td class="is-narrow">
+                                               <div class="buttons are-small">
+                                                       <a class="button is-dark"
+                                                                       download :href="`/packages/${pkg.uuid}/download${file.path}`">
+                                                               <Icon icon="download" :title="$t('Download')" />
+                                                       </a>
+                                               </div>
+                                       </td>
+                               </tr>
+                       </tbody>
+               </table>
+       </Section>
+</template>
index 67d5acf00787a365bc6ee875571b9336700ef578..d1ab4c782f5d0450e46f663f95f8822475a797ee 100644 (file)
@@ -3,6 +3,7 @@ import { library } from "@fortawesome/fontawesome-svg-core";
 
 // Only import the icons we actually need
 import {
+       faDownload,
        faLock,
        faPlugCircleXmark,
        faUser,
@@ -10,6 +11,7 @@ import {
 
 // Add them all to the library
 library.add(
+       faDownload,
        faLock,
        faPlugCircleXmark,
        faUser,
index f8798fbb0d2ef24d48bca00d3a344fd3bd20b474..dd9125a22845cfb4c866bd4c37cffa3637ed1b87 100644 (file)
@@ -23,3 +23,71 @@ export function formatHostname(url: string): string {
                return url;
        }
 }
+
+export function formatMode(mode: number): string {
+       const S_IFMT   = 0o170000;
+       const S_IFDIR  = 0o040000;
+       const S_IFREG  = 0o100000;
+       const S_IFLNK  = 0o120000;
+       const S_IFCHR  = 0o020000;
+       const S_IFBLK  = 0o060000;
+       const S_IFIFO  = 0o010000;
+       const S_IFSOCK = 0o140000;
+
+       const types: { [key: number]: string } = {
+               [S_IFREG]:  "-",
+               [S_IFDIR]:  "d",
+               [S_IFLNK]:  "l",
+               [S_IFCHR]:  "c",
+               [S_IFBLK]:  "b",
+               [S_IFIFO]:  "p",
+               [S_IFSOCK]: "s",
+       };
+
+       // Determine file type
+       const type = types[mode & S_IFMT] || "?";
+
+       // Permission characters helper
+       function symbol(mode: number, shift: number, char: string): string {
+               return (mode & (1 << shift)) ? char : "-";
+       }
+
+       // User permissions
+       const u_rd = symbol(mode, 8, "r");
+       const u_wr = symbol(mode, 7, "w");
+       let u_exec = symbol(mode, 6, "x");
+
+       // Group permissions
+       const g_rd = symbol(mode, 5, "r");
+       const g_wr = symbol(mode, 4, "w");
+       let g_exec = symbol(mode, 3, "x");
+
+       // Other permissions
+       const o_rd = symbol(mode, 2, "r");
+       const o_wr = symbol(mode, 1, "w");
+       let o_exec = symbol(mode, 0, "x");
+
+       // Special bits
+
+       // setuid (4000)
+       if (mode & 0o4000) {
+               u_exec = (u_exec === "x") ? "s" : "S";
+       }
+
+       // setgid (2000)
+       if (mode & 0o2000) {
+               g_exec = (g_exec === "x") ? "s" : "S";
+       }
+
+       // sticky bit (1000)
+       if (mode & 0o1000) {
+               o_exec = (o_exec === "x") ? "t" : "T";
+       }
+
+       return (
+               type +
+               u_rd + u_wr + u_exec +
+               g_rd + g_wr + g_exec +
+               o_rd + o_wr + o_exec
+       );
+}
index 844254b98727cd960ab5b4a9e5e5803e7fedf80a..83bbc08604e7a542a07f017212ad3e1642a72e68 100644 (file)
@@ -7,6 +7,7 @@
 
        // Import UI components
        import PackageHeader from "@/components/PackageHeader.vue";
+       import PackageFilelist from "@/components/PackageFilelist.vue";
 
        // Fetch the package UUID from the URL
        const route = useRoute();
@@ -23,4 +24,7 @@
 <template>
        <!-- Show the header -->
        <PackageHeader v-if="pkg" :pkg="pkg" />
+
+       <!-- Show the filelist -->
+       <PackageFilelist v-if="pkg" :pkg="pkg" />
 </template>