Compare commits
No commits in common. "5455e9b94cffe3d791f0cd2afbcceccf25bcdbbf" and "0a672619ea8d28df2b90c58d49487ae2c1252277" have entirely different histories.
5455e9b94c
...
0a672619ea
@ -12,8 +12,6 @@ import {
|
|||||||
CheckCircle2,
|
CheckCircle2,
|
||||||
Save,
|
Save,
|
||||||
Loader2,
|
Loader2,
|
||||||
Trash2,
|
|
||||||
AlertTriangle,
|
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { Layout } from "@/components/layout/Layout";
|
import { Layout } from "@/components/layout/Layout";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
@ -137,44 +135,13 @@ const QuotaEditModal = ({
|
|||||||
const StorageDashboard = (): ReactElement => {
|
const StorageDashboard = (): ReactElement => {
|
||||||
const { primaryColor } = useAppTheme();
|
const { primaryColor } = useAppTheme();
|
||||||
|
|
||||||
const [activeTab, setActiveTab] = useState<"stats" | "quota" | "cleanup">("stats");
|
const [activeTab, setActiveTab] = useState<"stats" | "quota">("stats");
|
||||||
const [stats, setStats] = useState<StorageStats | null>(null);
|
const [stats, setStats] = useState<StorageStats | null>(null);
|
||||||
const [quota, setQuota] = useState<StorageQuota | null>(null);
|
const [quota, setQuota] = useState<StorageQuota | null>(null);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const [isEditModalOpen, setIsEditModalOpen] = useState(false);
|
const [isEditModalOpen, setIsEditModalOpen] = useState(false);
|
||||||
|
|
||||||
// Storage Cleanup (purgeSoftDeletedBlobs) states
|
|
||||||
const [olderThanHours, setOlderThanHours] = useState(24);
|
|
||||||
const [isPurging, setIsPurging] = useState(false);
|
|
||||||
const [purgeResult, setPurgeResult] = useState<number | null>(null);
|
|
||||||
const [purgeError, setPurgeError] = useState<string | null>(null);
|
|
||||||
const [isConfirmModalOpen, setIsConfirmModalOpen] = useState(false);
|
|
||||||
|
|
||||||
const handlePurge = async () => {
|
|
||||||
setIsConfirmModalOpen(false);
|
|
||||||
setIsPurging(true);
|
|
||||||
setPurgeResult(null);
|
|
||||||
setPurgeError(null);
|
|
||||||
try {
|
|
||||||
const res = await fileAttachmentService.purgeSoftDeletedBlobs(olderThanHours);
|
|
||||||
if (res.success) {
|
|
||||||
setPurgeResult(res.data.purged);
|
|
||||||
// Refresh usage stats and quota following successful purge
|
|
||||||
await loadData();
|
|
||||||
} else {
|
|
||||||
setPurgeError("Failed to execute purge on the storage provider.");
|
|
||||||
}
|
|
||||||
} catch (err: any) {
|
|
||||||
setPurgeError(
|
|
||||||
err?.response?.data?.error?.message || "Failed to purge unreferenced files."
|
|
||||||
);
|
|
||||||
console.error("Purge error:", err);
|
|
||||||
} finally {
|
|
||||||
setIsPurging(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const loadData = async () => {
|
const loadData = async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
@ -276,22 +243,6 @@ const StorageDashboard = (): ReactElement => {
|
|||||||
>
|
>
|
||||||
Quota Details
|
Quota Details
|
||||||
</button>
|
</button>
|
||||||
<button
|
|
||||||
onClick={() => setActiveTab("cleanup")}
|
|
||||||
className={cn(
|
|
||||||
"px-6 py-3 text-sm font-bold transition-all border-b-2",
|
|
||||||
activeTab === "cleanup"
|
|
||||||
? "text-[#0e1b2a]"
|
|
||||||
: "border-transparent text-[#9aa6b2] hover:text-[#475569]",
|
|
||||||
)}
|
|
||||||
style={
|
|
||||||
activeTab === "cleanup"
|
|
||||||
? { borderBottomColor: primaryColor, color: primaryColor }
|
|
||||||
: {}
|
|
||||||
}
|
|
||||||
>
|
|
||||||
Storage Cleanup
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{activeTab === "stats" && (
|
{activeTab === "stats" && (
|
||||||
@ -511,129 +462,7 @@ const StorageDashboard = (): ReactElement => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{activeTab === "cleanup" && (
|
|
||||||
<div className="space-y-6 animate-in slide-in-from-bottom-2 duration-300">
|
|
||||||
<div className="bg-white border border-[rgba(0,0,0,0.08)] rounded-2xl overflow-hidden shadow-sm">
|
|
||||||
<div className="px-6 py-5 border-b border-[rgba(0,0,0,0.08)] flex items-center gap-2">
|
|
||||||
<Trash2
|
|
||||||
className="w-5 h-5 text-red-500"
|
|
||||||
/>
|
|
||||||
<h3 className="text-base font-black text-[#0e1b2a]">
|
|
||||||
Storage Maintenance & Cleanup
|
|
||||||
</h3>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="p-6 space-y-6">
|
|
||||||
<div className="max-w-2xl">
|
|
||||||
<p className="text-sm text-[#475569] leading-relaxed">
|
|
||||||
When files are deleted, the system decrements their reference counts. Files marked as{" "}
|
|
||||||
<strong>soft-deleted with 0 active references</strong> are preserved temporarily on the storage provider
|
|
||||||
to allow background services (like AI embeddings, audits, or indexes) to finish processing them.
|
|
||||||
</p>
|
|
||||||
<p className="text-sm text-[#475569] mt-3 leading-relaxed">
|
|
||||||
Use this administrative tool to permanently purge these unreferenced physical files from the storage provider and reclaim space.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="border border-red-100 rounded-xl p-5 bg-red-50/30 max-w-2xl flex gap-4">
|
|
||||||
<AlertTriangle className="w-5 h-5 text-red-500 shrink-0 mt-0.5" />
|
|
||||||
<div>
|
|
||||||
<h4 className="text-sm font-bold text-red-800">Warning: Permanent Action</h4>
|
|
||||||
<p className="text-xs text-red-700 mt-1 leading-relaxed">
|
|
||||||
Purging files is irreversible. Once deleted from the physical storage provider (local disk, Azure, or AWS),
|
|
||||||
the file binaries cannot be recovered.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="max-w-md pt-2 space-y-4">
|
|
||||||
<FormField
|
|
||||||
label="Purge Files Older Than (Hours)"
|
|
||||||
type="number"
|
|
||||||
value={olderThanHours}
|
|
||||||
onChange={(e) => setOlderThanHours(Math.max(0, parseInt(e.target.value) || 0))}
|
|
||||||
placeholder="e.g. 24 (0 = purge all eligible immediately)"
|
|
||||||
/>
|
|
||||||
<span className="text-[11px] text-[#9aa6b2] block -mt-2">
|
|
||||||
Set to 0 to instantly purge all unreferenced soft-deleted blobs, or specify hours (e.g., 24) to keep files deleted within that timeframe.
|
|
||||||
</span>
|
|
||||||
|
|
||||||
{purgeResult !== null && (
|
|
||||||
<div className="p-4 rounded-xl bg-green-50 border border-green-200 text-green-800 text-sm flex items-center gap-2">
|
|
||||||
<CheckCircle2 className="w-4.5 h-4.5 text-green-600 shrink-0" />
|
|
||||||
<span>
|
|
||||||
Successfully purged <strong>{purgeResult}</strong> orphaned file binary/binaries, freeing up storage space!
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{purgeError && (
|
|
||||||
<div className="p-4 rounded-xl bg-red-50 border border-red-200 text-red-800 text-sm">
|
|
||||||
{purgeError}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="pt-2">
|
|
||||||
<PrimaryButton
|
|
||||||
onClick={() => setIsConfirmModalOpen(true)}
|
|
||||||
disabled={isPurging}
|
|
||||||
className="bg-red-600 hover:bg-red-700 text-white font-bold flex items-center gap-2 border-red-600 shadow-sm"
|
|
||||||
>
|
|
||||||
{isPurging ? (
|
|
||||||
<Loader2 className="w-4 h-4 animate-spin" />
|
|
||||||
) : (
|
|
||||||
<Trash2 className="w-4 h-4" />
|
|
||||||
)}
|
|
||||||
Purge Unreferenced Files
|
|
||||||
</PrimaryButton>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{isConfirmModalOpen && (
|
|
||||||
<Modal
|
|
||||||
isOpen={isConfirmModalOpen}
|
|
||||||
onClose={() => setIsConfirmModalOpen(false)}
|
|
||||||
title="Confirm Storage Purge"
|
|
||||||
maxWidth="sm"
|
|
||||||
footer={
|
|
||||||
<div className="flex gap-2 justify-end w-full">
|
|
||||||
<button
|
|
||||||
onClick={() => setIsConfirmModalOpen(false)}
|
|
||||||
disabled={isPurging}
|
|
||||||
className="px-4 py-2 border border-gray-300 rounded-xl text-sm font-medium hover:bg-gray-50 disabled:opacity-50 transition-colors"
|
|
||||||
>
|
|
||||||
Cancel
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={handlePurge}
|
|
||||||
disabled={isPurging}
|
|
||||||
className="px-4 py-2 bg-red-600 hover:bg-red-700 text-white rounded-xl text-sm font-bold flex items-center gap-2 disabled:opacity-50 transition-colors shadow-sm"
|
|
||||||
>
|
|
||||||
{isPurging && <Loader2 className="w-4 h-4 animate-spin" />}
|
|
||||||
Yes, Purge Permanently
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<div className="p-6 space-y-4">
|
|
||||||
<div className="w-12 h-12 rounded-full bg-red-100 flex items-center justify-center mx-auto">
|
|
||||||
<AlertTriangle className="w-6 h-6 text-red-600" />
|
|
||||||
</div>
|
|
||||||
<div className="text-center space-y-2">
|
|
||||||
<h3 className="text-base font-bold text-gray-900">Are you absolutely sure?</h3>
|
|
||||||
<p className="text-sm text-gray-500 leading-relaxed">
|
|
||||||
This will permanently delete all unreferenced physical file binaries older than{" "}
|
|
||||||
<strong>{olderThanHours} hours</strong> from your storage provider. This action is irreversible.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Modal>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{isEditModalOpen && (
|
{isEditModalOpen && (
|
||||||
<QuotaEditModal
|
<QuotaEditModal
|
||||||
|
|||||||
@ -30,8 +30,6 @@ export interface FileAttachment {
|
|||||||
version: number;
|
version: number;
|
||||||
is_current_version: boolean;
|
is_current_version: boolean;
|
||||||
previous_version_id: string | null;
|
previous_version_id: string | null;
|
||||||
/** Reference to the physical file blob (dedup model) */
|
|
||||||
blob_id: string | null;
|
|
||||||
is_public: boolean;
|
is_public: boolean;
|
||||||
access_level: string;
|
access_level: string;
|
||||||
download_count: number;
|
download_count: number;
|
||||||
@ -269,18 +267,9 @@ export const fileAttachmentService = {
|
|||||||
return response.data;
|
return response.data;
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/** DELETE /files/:id */
|
||||||
* DELETE /files/:id
|
delete: async (id: string, hard = false): Promise<{ success: boolean }> => {
|
||||||
* @param hard - true: remove reference row from DB; false (default): soft-delete
|
const response = await apiClient.delete(`/files/${id}`, { params: { hard } });
|
||||||
* @param keepBlob - true: keep binary on disk even if last reference (Layer 3)
|
|
||||||
*/
|
|
||||||
delete: async (id: string, hard = false, keepBlob = false): Promise<{ success: boolean }> => {
|
|
||||||
const response = await apiClient.delete(`/files/${id}`, {
|
|
||||||
params: {
|
|
||||||
...(hard && { hard: 'true' }),
|
|
||||||
...(keepBlob && { keep_blob: 'true' }),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
return response.data;
|
return response.data;
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -398,18 +387,6 @@ export const fileAttachmentService = {
|
|||||||
}>(`/files/${id}/content`);
|
}>(`/files/${id}/content`);
|
||||||
return response.data;
|
return response.data;
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
|
||||||
* POST /files/blobs/purge-soft-deleted — admin only.
|
|
||||||
* Purges soft-deleted blobs with zero references older than olderThanHours.
|
|
||||||
* @param olderThanHours - 0 means purge all eligible blobs regardless of age.
|
|
||||||
*/
|
|
||||||
purgeSoftDeletedBlobs: async (olderThanHours = 0): Promise<{ success: boolean; data: { purged: number } }> => {
|
|
||||||
const response = await apiClient.post('/files/blobs/purge-soft-deleted', {}, {
|
|
||||||
params: olderThanHours > 0 ? { older_than_hours: olderThanHours } : {},
|
|
||||||
});
|
|
||||||
return response.data;
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default fileAttachmentService;
|
export default fileAttachmentService;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user