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;
+}
--- /dev/null
+<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>
+ ‐
+ </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>
// Only import the icons we actually need
import {
+ faDownload,
faLock,
faPlugCircleXmark,
faUser,
// Add them all to the library
library.add(
+ faDownload,
faLock,
faPlugCircleXmark,
faUser,
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
+ );
+}
// 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();
<template>
<!-- Show the header -->
<PackageHeader v-if="pkg" :pkg="pkg" />
+
+ <!-- Show the filelist -->
+ <PackageFilelist v-if="pkg" :pkg="pkg" />
</template>