mirror of
https://github.com/v0l/route96.git
synced 2025-06-14 15:46:32 +00:00
Compare commits
2 Commits
0554f1220f
...
d3711ff52c
Author | SHA1 | Date | |
---|---|---|---|
d3711ff52c | |||
fc080b5cd0 |
829
Cargo.lock
generated
829
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
28
migrations/20250610135841_reports.sql
Normal file
28
migrations/20250610135841_reports.sql
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
-- Create reports table for file reporting functionality
|
||||||
|
create table reports
|
||||||
|
(
|
||||||
|
id integer unsigned not null auto_increment primary key,
|
||||||
|
file_id binary(32) not null,
|
||||||
|
reporter_id integer unsigned not null,
|
||||||
|
event_json text not null,
|
||||||
|
created timestamp default current_timestamp,
|
||||||
|
|
||||||
|
constraint fk_reports_file
|
||||||
|
foreign key (file_id) references uploads (id)
|
||||||
|
on delete cascade
|
||||||
|
on update restrict,
|
||||||
|
|
||||||
|
constraint fk_reports_reporter
|
||||||
|
foreign key (reporter_id) references users (id)
|
||||||
|
on delete cascade
|
||||||
|
on update restrict
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Unique index to prevent duplicate reports from same user for same file
|
||||||
|
create unique index ix_reports_file_reporter on reports (file_id, reporter_id);
|
||||||
|
|
||||||
|
-- Index for efficient lookups by file
|
||||||
|
create index ix_reports_file_id on reports (file_id);
|
||||||
|
|
||||||
|
-- Index for efficient lookups by reporter
|
||||||
|
create index ix_reports_reporter_id on reports (reporter_id);
|
5
migrations/20250610140000_add_reviewed_flag.sql
Normal file
5
migrations/20250610140000_add_reviewed_flag.sql
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
-- Add reviewed flag to reports table
|
||||||
|
alter table reports add column reviewed boolean not null default false;
|
||||||
|
|
||||||
|
-- Index for efficient filtering of non-reviewed reports
|
||||||
|
create index ix_reports_reviewed on reports (reviewed);
|
59
src/db.rs
59
src/db.rs
@ -108,6 +108,17 @@ pub struct Payment {
|
|||||||
pub rate: Option<f32>,
|
pub rate: Option<f32>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, FromRow, Serialize)]
|
||||||
|
pub struct Report {
|
||||||
|
pub id: u64,
|
||||||
|
#[serde(with = "hex")]
|
||||||
|
pub file_id: Vec<u8>,
|
||||||
|
pub reporter_id: u64,
|
||||||
|
pub event_json: String,
|
||||||
|
pub created: DateTime<Utc>,
|
||||||
|
pub reviewed: bool,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct Database {
|
pub struct Database {
|
||||||
pub(crate) pool: sqlx::pool::Pool<sqlx::mysql::MySql>,
|
pub(crate) pool: sqlx::pool::Pool<sqlx::mysql::MySql>,
|
||||||
@ -369,4 +380,52 @@ impl Database {
|
|||||||
// Check if upload would exceed quota
|
// Check if upload would exceed quota
|
||||||
Ok(user_stats.total_size + upload_size <= available_quota)
|
Ok(user_stats.total_size + upload_size <= available_quota)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Add a new report to the database
|
||||||
|
pub async fn add_report(&self, file_id: &[u8], reporter_id: u64, event_json: &str) -> Result<(), Error> {
|
||||||
|
sqlx::query("insert into reports (file_id, reporter_id, event_json) values (?, ?, ?)")
|
||||||
|
.bind(file_id)
|
||||||
|
.bind(reporter_id)
|
||||||
|
.bind(event_json)
|
||||||
|
.execute(&self.pool)
|
||||||
|
.await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// List reports with pagination for admin view
|
||||||
|
pub async fn list_reports(&self, offset: u32, limit: u32) -> Result<(Vec<Report>, i64), Error> {
|
||||||
|
let reports: Vec<Report> = sqlx::query_as(
|
||||||
|
"select id, file_id, reporter_id, event_json, created, reviewed from reports where reviewed = false order by created desc limit ? offset ?"
|
||||||
|
)
|
||||||
|
.bind(limit)
|
||||||
|
.bind(offset)
|
||||||
|
.fetch_all(&self.pool)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let count: i64 = sqlx::query("select count(id) from reports where reviewed = false")
|
||||||
|
.fetch_one(&self.pool)
|
||||||
|
.await?
|
||||||
|
.try_get(0)?;
|
||||||
|
|
||||||
|
Ok((reports, count))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get reports for a specific file
|
||||||
|
pub async fn get_file_reports(&self, file_id: &[u8]) -> Result<Vec<Report>, Error> {
|
||||||
|
sqlx::query_as(
|
||||||
|
"select id, file_id, reporter_id, event_json, created, reviewed from reports where file_id = ? order by created desc"
|
||||||
|
)
|
||||||
|
.bind(file_id)
|
||||||
|
.fetch_all(&self.pool)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Mark a report as reviewed (used for acknowledging)
|
||||||
|
pub async fn mark_report_reviewed(&self, report_id: u64) -> Result<(), Error> {
|
||||||
|
sqlx::query("update reports set reviewed = true where id = ?")
|
||||||
|
.bind(report_id)
|
||||||
|
.execute(&self.pool)
|
||||||
|
.await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
use crate::auth::nip98::Nip98Auth;
|
use crate::auth::nip98::Nip98Auth;
|
||||||
use crate::db::{Database, FileUpload, User};
|
use crate::db::{Database, FileUpload, User, Report};
|
||||||
use crate::routes::{Nip94Event, PagedResult};
|
use crate::routes::{Nip94Event, PagedResult};
|
||||||
use crate::settings::Settings;
|
use crate::settings::Settings;
|
||||||
use rocket::serde::json::Json;
|
use rocket::serde::json::Json;
|
||||||
@ -8,7 +8,7 @@ use rocket::{routes, Responder, Route, State};
|
|||||||
use sqlx::{Error, QueryBuilder, Row};
|
use sqlx::{Error, QueryBuilder, Row};
|
||||||
|
|
||||||
pub fn admin_routes() -> Vec<Route> {
|
pub fn admin_routes() -> Vec<Route> {
|
||||||
routes![admin_list_files, admin_get_self]
|
routes![admin_list_files, admin_get_self, admin_list_reports, admin_acknowledge_report]
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Default)]
|
#[derive(Serialize, Default)]
|
||||||
@ -161,6 +161,59 @@ async fn admin_list_files(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[rocket::get("/reports?<page>&<count>")]
|
||||||
|
async fn admin_list_reports(
|
||||||
|
auth: Nip98Auth,
|
||||||
|
page: u32,
|
||||||
|
count: u32,
|
||||||
|
db: &State<Database>,
|
||||||
|
) -> AdminResponse<PagedResult<Report>> {
|
||||||
|
let pubkey_vec = auth.event.pubkey.to_bytes().to_vec();
|
||||||
|
let server_count = count.clamp(1, 5_000);
|
||||||
|
|
||||||
|
let user = match db.get_user(&pubkey_vec).await {
|
||||||
|
Ok(user) => user,
|
||||||
|
Err(_) => return AdminResponse::error("User not found"),
|
||||||
|
};
|
||||||
|
|
||||||
|
if !user.is_admin {
|
||||||
|
return AdminResponse::error("User is not an admin");
|
||||||
|
}
|
||||||
|
|
||||||
|
match db.list_reports(page * server_count, server_count).await {
|
||||||
|
Ok((reports, total_count)) => AdminResponse::success(PagedResult {
|
||||||
|
count: reports.len() as u32,
|
||||||
|
page,
|
||||||
|
total: total_count as u32,
|
||||||
|
files: reports,
|
||||||
|
}),
|
||||||
|
Err(e) => AdminResponse::error(&format!("Could not list reports: {}", e)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[rocket::delete("/reports/<report_id>")]
|
||||||
|
async fn admin_acknowledge_report(
|
||||||
|
auth: Nip98Auth,
|
||||||
|
report_id: u64,
|
||||||
|
db: &State<Database>,
|
||||||
|
) -> AdminResponse<()> {
|
||||||
|
let pubkey_vec = auth.event.pubkey.to_bytes().to_vec();
|
||||||
|
|
||||||
|
let user = match db.get_user(&pubkey_vec).await {
|
||||||
|
Ok(user) => user,
|
||||||
|
Err(_) => return AdminResponse::error("User not found"),
|
||||||
|
};
|
||||||
|
|
||||||
|
if !user.is_admin {
|
||||||
|
return AdminResponse::error("User is not an admin");
|
||||||
|
}
|
||||||
|
|
||||||
|
match db.mark_report_reviewed(report_id).await {
|
||||||
|
Ok(()) => AdminResponse::success(()),
|
||||||
|
Err(e) => AdminResponse::error(&format!("Could not acknowledge report: {}", e)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl Database {
|
impl Database {
|
||||||
pub async fn list_all_files(
|
pub async fn list_all_files(
|
||||||
&self,
|
&self,
|
||||||
|
@ -5,7 +5,7 @@ use crate::routes::{delete_file, Nip94Event};
|
|||||||
use crate::settings::Settings;
|
use crate::settings::Settings;
|
||||||
use log::error;
|
use log::error;
|
||||||
use nostr::prelude::hex;
|
use nostr::prelude::hex;
|
||||||
use nostr::{Alphabet, SingleLetterTag, TagKind};
|
use nostr::{Alphabet, SingleLetterTag, TagKind, JsonUtil};
|
||||||
use rocket::data::ByteUnit;
|
use rocket::data::ByteUnit;
|
||||||
use rocket::futures::StreamExt;
|
use rocket::futures::StreamExt;
|
||||||
use rocket::http::{Header, Status};
|
use rocket::http::{Header, Status};
|
||||||
@ -63,13 +63,14 @@ pub fn blossom_routes() -> Vec<Route> {
|
|||||||
upload_head,
|
upload_head,
|
||||||
upload_media,
|
upload_media,
|
||||||
head_media,
|
head_media,
|
||||||
mirror
|
mirror,
|
||||||
|
report_file
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(not(feature = "media-compression"))]
|
#[cfg(not(feature = "media-compression"))]
|
||||||
pub fn blossom_routes() -> Vec<Route> {
|
pub fn blossom_routes() -> Vec<Route> {
|
||||||
routes![delete_blob, upload, list_files, upload_head, mirror]
|
routes![delete_blob, upload, list_files, upload_head, mirror, report_file]
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Generic holder response, mostly for errors
|
/// Generic holder response, mostly for errors
|
||||||
@ -429,3 +430,65 @@ where
|
|||||||
BlossomResponse::BlobDescriptor(Json(BlobDescriptor::from_upload(settings, &upload)))
|
BlossomResponse::BlobDescriptor(Json(BlobDescriptor::from_upload(settings, &upload)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[rocket::put("/report", data = "<data>", format = "json")]
|
||||||
|
async fn report_file(
|
||||||
|
auth: BlossomAuth,
|
||||||
|
db: &State<Database>,
|
||||||
|
settings: &State<Settings>,
|
||||||
|
data: Json<nostr::Event>,
|
||||||
|
) -> BlossomResponse {
|
||||||
|
// Check if the request has the correct method tag
|
||||||
|
if !check_method(&auth.event, "report") {
|
||||||
|
return BlossomResponse::error("Invalid request method tag");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check whitelist
|
||||||
|
if let Some(e) = check_whitelist(&auth, settings) {
|
||||||
|
return e;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract file SHA256 from the "x" tag in the report event
|
||||||
|
let file_sha256 = if let Some(x_tag) = data.tags.iter().find_map(|t| {
|
||||||
|
if t.kind() == TagKind::SingleLetter(SingleLetterTag::lowercase(Alphabet::X)) {
|
||||||
|
t.content()
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}) {
|
||||||
|
match hex::decode(x_tag) {
|
||||||
|
Ok(hash) => hash,
|
||||||
|
Err(_) => return BlossomResponse::error("Invalid file hash in x tag"),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return BlossomResponse::error("Missing file hash in x tag");
|
||||||
|
};
|
||||||
|
|
||||||
|
// Verify the reported file exists
|
||||||
|
match db.get_file(&file_sha256).await {
|
||||||
|
Ok(Some(_)) => {}, // File exists, continue
|
||||||
|
Ok(None) => return BlossomResponse::error("File not found"),
|
||||||
|
Err(e) => return BlossomResponse::error(format!("Failed to check file: {}", e)),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get or create the reporter user
|
||||||
|
let reporter_id = match db.upsert_user(&auth.event.pubkey.to_bytes().to_vec()).await {
|
||||||
|
Ok(user_id) => user_id,
|
||||||
|
Err(e) => return BlossomResponse::error(format!("Failed to get user: {}", e)),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Store the report (the database will handle duplicate prevention via unique index)
|
||||||
|
match db.add_report(&file_sha256, reporter_id, &data.as_json()).await {
|
||||||
|
Ok(()) => BlossomResponse::Generic(BlossomGenericResponse {
|
||||||
|
status: Status::Ok,
|
||||||
|
message: Some("Report submitted successfully".to_string()),
|
||||||
|
}),
|
||||||
|
Err(e) => {
|
||||||
|
if e.to_string().contains("Duplicate entry") {
|
||||||
|
BlossomResponse::error("You have already reported this file")
|
||||||
|
} else {
|
||||||
|
BlossomResponse::error(format!("Failed to submit report: {}", e))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -12,6 +12,15 @@ export interface AdminSelf {
|
|||||||
total_available_quota?: number;
|
total_available_quota?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface Report {
|
||||||
|
id: number;
|
||||||
|
file_id: string;
|
||||||
|
reporter_id: number;
|
||||||
|
event_json: string;
|
||||||
|
created: string;
|
||||||
|
reviewed: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
export class Route96 {
|
export class Route96 {
|
||||||
constructor(
|
constructor(
|
||||||
readonly url: string,
|
readonly url: string,
|
||||||
@ -39,6 +48,25 @@ export class Route96 {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async listReports(page = 0, count = 10) {
|
||||||
|
const rsp = await this.#req(
|
||||||
|
`admin/reports?page=${page}&count=${count}`,
|
||||||
|
"GET",
|
||||||
|
);
|
||||||
|
const data = await this.#handleResponse<AdminResponseReportList>(rsp);
|
||||||
|
return {
|
||||||
|
...data,
|
||||||
|
...data.data,
|
||||||
|
files: data.data.files,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async acknowledgeReport(reportId: number) {
|
||||||
|
const rsp = await this.#req(`admin/reports/${reportId}`, "DELETE");
|
||||||
|
const data = await this.#handleResponse<AdminResponse<void>>(rsp);
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
async #handleResponse<T extends AdminResponseBase>(rsp: Response) {
|
async #handleResponse<T extends AdminResponseBase>(rsp: Response) {
|
||||||
if (rsp.ok) {
|
if (rsp.ok) {
|
||||||
return (await rsp.json()) as T;
|
return (await rsp.json()) as T;
|
||||||
@ -94,3 +122,10 @@ export type AdminResponseFileList = AdminResponse<{
|
|||||||
count: number;
|
count: number;
|
||||||
files: Array<NostrEvent>;
|
files: Array<NostrEvent>;
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
|
export type AdminResponseReportList = AdminResponse<{
|
||||||
|
total: number;
|
||||||
|
page: number;
|
||||||
|
count: number;
|
||||||
|
files: Array<Report>;
|
||||||
|
}>;
|
||||||
|
141
ui_src/src/views/reports.tsx
Normal file
141
ui_src/src/views/reports.tsx
Normal file
@ -0,0 +1,141 @@
|
|||||||
|
import { NostrLink } from "@snort/system";
|
||||||
|
import classNames from "classnames";
|
||||||
|
import Profile from "../components/profile";
|
||||||
|
import { Report } from "../upload/admin";
|
||||||
|
|
||||||
|
export default function ReportList({
|
||||||
|
reports,
|
||||||
|
pages,
|
||||||
|
page,
|
||||||
|
onPage,
|
||||||
|
onAcknowledge,
|
||||||
|
onDeleteFile,
|
||||||
|
}: {
|
||||||
|
reports: Array<Report>;
|
||||||
|
pages?: number;
|
||||||
|
page?: number;
|
||||||
|
onPage?: (n: number) => void;
|
||||||
|
onAcknowledge?: (reportId: number) => void;
|
||||||
|
onDeleteFile?: (fileId: string) => void;
|
||||||
|
}) {
|
||||||
|
if (reports.length === 0) {
|
||||||
|
return <b>No Reports</b>;
|
||||||
|
}
|
||||||
|
|
||||||
|
function pageButtons(page: number, n: number) {
|
||||||
|
const ret = [];
|
||||||
|
const start = 0;
|
||||||
|
|
||||||
|
for (let x = start; x < n; x++) {
|
||||||
|
ret.push(
|
||||||
|
<div
|
||||||
|
key={x}
|
||||||
|
onClick={() => onPage?.(x)}
|
||||||
|
className={classNames(
|
||||||
|
"bg-neutral-700 hover:bg-neutral-600 min-w-8 text-center cursor-pointer font-bold",
|
||||||
|
{
|
||||||
|
"rounded-l-md": x === start,
|
||||||
|
"rounded-r-md": x + 1 === n,
|
||||||
|
"bg-neutral-400": page === x,
|
||||||
|
},
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{x + 1}
|
||||||
|
</div>,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getReporterPubkey(eventJson: string): string | null {
|
||||||
|
try {
|
||||||
|
const event = JSON.parse(eventJson);
|
||||||
|
return event.pubkey;
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getReportReason(eventJson: string): string {
|
||||||
|
try {
|
||||||
|
const event = JSON.parse(eventJson);
|
||||||
|
return event.content || "No reason provided";
|
||||||
|
} catch {
|
||||||
|
return "Invalid event data";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatDate(dateString: string): string {
|
||||||
|
return new Date(dateString).toLocaleString();
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<table className="w-full border-collapse border border-neutral-500">
|
||||||
|
<thead>
|
||||||
|
<tr className="bg-neutral-700">
|
||||||
|
<th className="border border-neutral-500 py-2 px-4 text-left">Report ID</th>
|
||||||
|
<th className="border border-neutral-500 py-2 px-4 text-left">File ID</th>
|
||||||
|
<th className="border border-neutral-500 py-2 px-4 text-left">Reporter</th>
|
||||||
|
<th className="border border-neutral-500 py-2 px-4 text-left">Reason</th>
|
||||||
|
<th className="border border-neutral-500 py-2 px-4 text-left">Created</th>
|
||||||
|
<th className="border border-neutral-500 py-2 px-4 text-left">Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{reports.map((report) => {
|
||||||
|
const reporterPubkey = getReporterPubkey(report.event_json);
|
||||||
|
const reason = getReportReason(report.event_json);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<tr key={report.id} className="hover:bg-neutral-700">
|
||||||
|
<td className="border border-neutral-500 py-2 px-4">{report.id}</td>
|
||||||
|
<td className="border border-neutral-500 py-2 px-4 font-mono text-sm">
|
||||||
|
{report.file_id.substring(0, 12)}...
|
||||||
|
</td>
|
||||||
|
<td className="border border-neutral-500 py-2 px-4">
|
||||||
|
{reporterPubkey ? (
|
||||||
|
<Profile link={NostrLink.publicKey(reporterPubkey)} size={20} />
|
||||||
|
) : (
|
||||||
|
"Unknown"
|
||||||
|
)}
|
||||||
|
</td>
|
||||||
|
<td className="border border-neutral-500 py-2 px-4 max-w-xs truncate">
|
||||||
|
{reason}
|
||||||
|
</td>
|
||||||
|
<td className="border border-neutral-500 py-2 px-4">
|
||||||
|
{formatDate(report.created)}
|
||||||
|
</td>
|
||||||
|
<td className="border border-neutral-500 py-2 px-4">
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<button
|
||||||
|
onClick={() => onAcknowledge?.(report.id)}
|
||||||
|
className="bg-blue-600 hover:bg-blue-700 px-2 py-1 rounded text-sm"
|
||||||
|
>
|
||||||
|
Acknowledge
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => onDeleteFile?.(report.file_id)}
|
||||||
|
className="bg-red-600 hover:bg-red-700 px-2 py-1 rounded text-sm"
|
||||||
|
>
|
||||||
|
Delete File
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
{pages !== undefined && (
|
||||||
|
<>
|
||||||
|
<div className="flex justify-center mt-4">
|
||||||
|
<div className="flex gap-1">{pageButtons(page ?? 0, pages)}</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
@ -8,16 +8,13 @@ import usePublisher from "../hooks/publisher";
|
|||||||
import { Nip96, Nip96FileList } from "../upload/nip96";
|
import { Nip96, Nip96FileList } from "../upload/nip96";
|
||||||
import { AdminSelf, Route96 } from "../upload/admin";
|
import { AdminSelf, Route96 } from "../upload/admin";
|
||||||
import { FormatBytes } from "../const";
|
import { FormatBytes } from "../const";
|
||||||
import Report from "../report.json";
|
|
||||||
|
|
||||||
export default function Upload() {
|
export default function Upload() {
|
||||||
const [type, setType] = useState<"blossom" | "nip96">("blossom");
|
const [type, setType] = useState<"blossom" | "nip96">("blossom");
|
||||||
const [noCompress, setNoCompress] = useState(false);
|
const [noCompress, setNoCompress] = useState(false);
|
||||||
const [showLegacy, setShowLegacy] = useState(false);
|
|
||||||
const [toUpload, setToUpload] = useState<File>();
|
const [toUpload, setToUpload] = useState<File>();
|
||||||
const [self, setSelf] = useState<AdminSelf>();
|
const [self, setSelf] = useState<AdminSelf>();
|
||||||
const [error, setError] = useState<string>();
|
const [error, setError] = useState<string>();
|
||||||
const [bulkPrgress, setBulkProgress] = useState<number>();
|
|
||||||
const [results, setResults] = useState<Array<object>>([]);
|
const [results, setResults] = useState<Array<object>>([]);
|
||||||
const [listedFiles, setListedFiles] = useState<Nip96FileList>();
|
const [listedFiles, setListedFiles] = useState<Nip96FileList>();
|
||||||
const [adminListedFiles, setAdminListedFiles] = useState<Nip96FileList>();
|
const [adminListedFiles, setAdminListedFiles] = useState<Nip96FileList>();
|
||||||
@ -28,9 +25,6 @@ export default function Upload() {
|
|||||||
const login = useLogin();
|
const login = useLogin();
|
||||||
const pub = usePublisher();
|
const pub = usePublisher();
|
||||||
|
|
||||||
const legacyFiles = Report as Record<string, Array<string>>;
|
|
||||||
const myLegacyFiles = login ? (legacyFiles[login.pubkey] ?? []) : [];
|
|
||||||
|
|
||||||
const url =
|
const url =
|
||||||
import.meta.env.VITE_API_URL || `${location.protocol}//${location.host}`;
|
import.meta.env.VITE_API_URL || `${location.protocol}//${location.host}`;
|
||||||
async function doUpload() {
|
async function doUpload() {
|
||||||
@ -116,20 +110,6 @@ export default function Upload() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function migrateLegacy() {
|
|
||||||
if (!pub) return;
|
|
||||||
const uploader = new Blossom(url, pub);
|
|
||||||
let ctr = 0;
|
|
||||||
for (const f of myLegacyFiles) {
|
|
||||||
try {
|
|
||||||
await uploader.mirror(`https://void.cat/d/${f}`);
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e);
|
|
||||||
}
|
|
||||||
setBulkProgress(ctr++ / myLegacyFiles.length);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
listUploads(listedPage);
|
listUploads(listedPage);
|
||||||
}, [listedPage]);
|
}, [listedPage]);
|
||||||
@ -211,27 +191,6 @@ export default function Upload() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{login && myLegacyFiles.length > 0 && (
|
|
||||||
<div className="flex flex-col gap-4 font-bold">
|
|
||||||
You have {myLegacyFiles.length.toLocaleString()} files which can be
|
|
||||||
migrated from void.cat
|
|
||||||
<div className="flex gap-2">
|
|
||||||
<Button onClick={() => migrateLegacy()}>Migrate Files</Button>
|
|
||||||
<Button onClick={() => setShowLegacy((s) => !s)}>
|
|
||||||
{!showLegacy ? "Show Files" : "Hide Files"}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
{bulkPrgress !== undefined && <progress value={bulkPrgress} />}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{showLegacy && (
|
|
||||||
<FileList
|
|
||||||
files={myLegacyFiles.map((f) => ({
|
|
||||||
id: f,
|
|
||||||
url: `https://void.cat/d/${f}`,
|
|
||||||
}))}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{listedFiles && (
|
{listedFiles && (
|
||||||
<FileList
|
<FileList
|
||||||
files={listedFiles.files}
|
files={listedFiles.files}
|
||||||
|
@ -1 +1 @@
|
|||||||
{"root":["./src/App.tsx","./src/const.ts","./src/login.ts","./src/main.tsx","./src/vite-env.d.ts","./src/components/button.tsx","./src/components/profile.tsx","./src/hooks/login.ts","./src/hooks/publisher.ts","./src/upload/admin.ts","./src/upload/blossom.ts","./src/upload/index.ts","./src/upload/nip96.ts","./src/views/files.tsx","./src/views/header.tsx","./src/views/upload.tsx"],"version":"5.6.2"}
|
{"root":["./src/App.tsx","./src/const.ts","./src/login.ts","./src/main.tsx","./src/vite-env.d.ts","./src/components/button.tsx","./src/components/profile.tsx","./src/hooks/login.ts","./src/hooks/publisher.ts","./src/upload/admin.ts","./src/upload/blossom.ts","./src/upload/index.ts","./src/upload/nip96.ts","./src/views/files.tsx","./src/views/header.tsx","./src/views/reports.tsx","./src/views/upload.tsx"],"version":"5.6.2"}
|
Reference in New Issue
Block a user