1
0

Add file lookup utility page (#4276)

* Add file lookup utility page

* Lint
This commit is contained in:
Prospector
2025-08-28 14:52:43 -07:00
committed by GitHub
parent d30643b5a0
commit df261dad95
2 changed files with 210 additions and 0 deletions

View File

@@ -302,6 +302,13 @@
color: 'orange',
link: '/moderation/reports',
},
{
divider: true,
},
{
id: 'file-lookup',
link: '/admin/file_lookup',
},
{
divider: true,
shown: isAdmin(auth.user),
@@ -325,6 +332,7 @@
<template #review-projects> <ScaleIcon aria-hidden="true" /> Review projects </template>
<template #review-reports> <ReportIcon aria-hidden="true" /> Reports </template>
<template #user-lookup> <UserIcon aria-hidden="true" /> Lookup by email </template>
<template #file-lookup> <FileIcon aria-hidden="true" /> File lookup </template>
<template #servers-notices>
<IssuesIcon aria-hidden="true" /> Manage server notices
</template>
@@ -679,6 +687,7 @@ import {
DiscordIcon,
DownloadIcon,
DropdownIcon,
FileIcon,
GithubIcon,
GlassesIcon,
HamburgerIcon,

View File

@@ -0,0 +1,201 @@
<template>
<div class="normal-page no-sidebar">
<h1>File lookup</h1>
<div class="normal-page__content">
<div class="card flex flex-col gap-3">
<div
class="border-highlight-gray hover:bg-button-hover relative flex h-32 cursor-pointer flex-col items-center justify-center rounded-xl border-2 border-dashed bg-button-bg p-8 transition-colors"
@click="triggerFileInput"
@drop.prevent="handleDrop"
@dragover.prevent
@dragenter.prevent
>
<p
class="mx-auto mb-0 flex items-center gap-2 text-center text-lg font-bold text-primary"
>
<UploadIcon /> Select file to lookup
</p>
<p class="mx-auto mt-0 text-center text-sm text-secondary">
Drag and drop or click here to browse
</p>
<input ref="fileInput" type="file" class="hidden" @change="handleFileSelect" />
</div>
<template v-if="selectedFile">
<div class="flex items-center gap-2 text-sm text-secondary">
<FileIcon class="h-4 w-4" />
<span>{{ selectedFile.name }} ({{ formatBytes(selectedFile.size) }})</span>
</div>
<div v-if="loadingHash" class="flex items-center gap-2 text-sm text-secondary">
<SpinnerIcon class="h-4 w-4 animate-spin" />
Calculating hashes...
</div>
<div v-if="loadingLookup" class="flex items-center gap-2 text-sm text-secondary">
<SpinnerIcon class="h-4 w-4 animate-spin" />
Looking up file on Modrinth...
</div>
<template v-if="fileHashes">
<h3 class="mb-0 text-lg font-extrabold text-contrast">File hashes:</h3>
<div class="flex flex-col gap-2">
<span class="text-xs text-secondary">SHA512:</span>
<CopyCode :text="fileHashes.sha512" />
<span class="mt-1 text-xs text-secondary">SHA256:</span>
<CopyCode :text="fileHashes.sha256" />
<span class="mt-1 text-xs text-secondary">SHA1:</span>
<CopyCode :text="fileHashes.sha1" />
</div>
</template>
</template>
<template v-if="lookupResult">
<h3 class="mb-0 text-lg font-extrabold text-contrast">Modrinth project:</h3>
<nuxt-link
class="flex w-fit items-center gap-2 text-lg font-semibold text-contrast hover:underline"
target="_blank"
:to="`/project/${lookupResult.projectId}`"
>
<Avatar :src="lookupResult.iconUrl" alt="" size="48px" />
{{ lookupResult.name }}
</nuxt-link>
<CopyCode :text="lookupResult.projectId" />
<h3 class="mb-0 text-lg font-extrabold text-contrast">Modrinth version:</h3>
<nuxt-link
class="text-blue hover:underline"
:to="`/project/${lookupResult.projectId}/version/${lookupResult.versionId}`"
target="_blank"
>
Version {{ lookupResult.versionNumber }}
</nuxt-link>
<CopyCode :text="lookupResult.versionId" />
</template>
<Admonition v-if="lookupError" type="critical" header="Lookup failed">
{{ lookupError }}
</Admonition>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { FileIcon, SpinnerIcon, UploadIcon } from '@modrinth/assets'
import { Admonition, Avatar, CopyCode, injectNotificationManager } from '@modrinth/ui'
import { formatBytes, type Project, type Version } from '@modrinth/utils'
const { addNotification } = injectNotificationManager()
const fileInput = ref<HTMLInputElement>()
const selectedFile = ref<File | null>(null)
const fileHashes = ref<{
sha512: string
sha256: string
sha1: string
} | null>(null)
const loadingHash = ref(false)
const loadingLookup = ref(false)
const lookupResult = ref<{
projectId: string
versionId: string
name: string
versionNumber: string
iconUrl?: string | undefined
}>()
const lookupError = ref<string>('')
function triggerFileInput() {
fileInput.value?.click()
}
function handleFileSelect(event: Event) {
const target = event.target as HTMLInputElement
if (target.files && target.files.length > 0) {
processFile(target.files[0])
}
}
function handleDrop(event: DragEvent) {
event.preventDefault()
if (event.dataTransfer?.files && event.dataTransfer.files.length > 0) {
processFile(event.dataTransfer.files[0])
}
}
async function processFile(file: File) {
selectedFile.value = file
fileHashes.value = null
lookupResult.value = undefined
lookupError.value = ''
await calculateHashesAndLookup(file)
}
function formatHashBuffer(buffer: ArrayBuffer) {
return Array.from(new Uint8Array(buffer))
.map((b) => b.toString(16).padStart(2, '0'))
.join('')
}
async function calculateHashesAndLookup(file: File): Promise<void> {
loadingHash.value = true
loadingLookup.value = true
try {
const buffer = await file.arrayBuffer()
const [sha512, sha256, sha1] = await Promise.all([
crypto.subtle.digest('SHA-512', buffer).then(formatHashBuffer),
crypto.subtle.digest('SHA-256', buffer).then(formatHashBuffer),
crypto.subtle.digest('SHA-1', buffer).then(formatHashBuffer),
])
fileHashes.value = { sha512, sha256, sha1 }
await lookupFile(sha512)
} catch (error) {
console.error('Error calculating hashes:', error)
addNotification({
title: 'Hash calculation failed',
text: 'Failed to calculate file hashes.',
type: 'error',
})
} finally {
loadingHash.value = false
loadingLookup.value = false
}
}
async function lookupFile(hash: string): Promise<void> {
if (!hash) {
return
}
try {
const version = (await useBaseFetch(`version_file/${hash}?algorithm=sha512`, {
method: 'GET',
})) as Version
if (version) {
const project = (await useBaseFetch(`project/${version.project_id}`, {
method: 'GET',
})) as Project
lookupResult.value = {
projectId: project.id,
versionId: version.id,
versionNumber: version.version_number,
name: project.title,
iconUrl: project.icon_url,
}
}
} catch (error: any) {
if (error.status === 404) {
lookupError.value = `File not found on Modrinth across projects you have access to.`
} else {
lookupError.value = error.data?.description || 'Failed to lookup file.'
}
}
}
</script>