feat: remove trash
This commit is contained in:
parent
fab5696726
commit
c7d68a55a8
|
|
@ -61,7 +61,6 @@ class AssetResponse(BaseModel):
|
||||||
storage_key_original: str
|
storage_key_original: str
|
||||||
storage_key_thumb: Optional[str] = None
|
storage_key_thumb: Optional[str] = None
|
||||||
created_at: datetime
|
created_at: datetime
|
||||||
deleted_at: Optional[datetime] = None
|
|
||||||
|
|
||||||
model_config = {"from_attributes": True}
|
model_config = {"from_attributes": True}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -145,7 +145,7 @@ async def stream_media(
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@router.delete("/{asset_id}", response_model=AssetResponse)
|
@router.delete("/{asset_id}", status_code=status.HTTP_204_NO_CONTENT)
|
||||||
async def delete_asset(
|
async def delete_asset(
|
||||||
asset_id: str,
|
asset_id: str,
|
||||||
current_user: CurrentUser,
|
current_user: CurrentUser,
|
||||||
|
|
@ -153,55 +153,7 @@ async def delete_asset(
|
||||||
s3_client: S3ClientDep,
|
s3_client: S3ClientDep,
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
Soft delete an asset (move to trash).
|
Delete an asset permanently (move to trash bucket, delete from DB).
|
||||||
|
|
||||||
Args:
|
|
||||||
asset_id: Asset ID
|
|
||||||
current_user: Current authenticated user
|
|
||||||
session: Database session
|
|
||||||
s3_client: S3 client
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Updated asset
|
|
||||||
"""
|
|
||||||
asset_service = AssetService(session, s3_client)
|
|
||||||
asset = await asset_service.delete_asset(user_id=current_user.id, asset_id=asset_id)
|
|
||||||
return asset
|
|
||||||
|
|
||||||
|
|
||||||
@router.post("/{asset_id}/restore", response_model=AssetResponse)
|
|
||||||
async def restore_asset(
|
|
||||||
asset_id: str,
|
|
||||||
current_user: CurrentUser,
|
|
||||||
session: DatabaseSession,
|
|
||||||
s3_client: S3ClientDep,
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
Restore a soft-deleted asset.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
asset_id: Asset ID
|
|
||||||
current_user: Current authenticated user
|
|
||||||
session: Database session
|
|
||||||
s3_client: S3 client
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Updated asset
|
|
||||||
"""
|
|
||||||
asset_service = AssetService(session, s3_client)
|
|
||||||
asset = await asset_service.restore_asset(user_id=current_user.id, asset_id=asset_id)
|
|
||||||
return asset
|
|
||||||
|
|
||||||
|
|
||||||
@router.delete("/{asset_id}/purge", status_code=status.HTTP_204_NO_CONTENT)
|
|
||||||
async def purge_asset(
|
|
||||||
asset_id: str,
|
|
||||||
current_user: CurrentUser,
|
|
||||||
session: DatabaseSession,
|
|
||||||
s3_client: S3ClientDep,
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
Permanently delete an asset.
|
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
asset_id: Asset ID
|
asset_id: Asset ID
|
||||||
|
|
@ -210,4 +162,4 @@ async def purge_asset(
|
||||||
s3_client: S3 client
|
s3_client: S3 client
|
||||||
"""
|
"""
|
||||||
asset_service = AssetService(session, s3_client)
|
asset_service = AssetService(session, s3_client)
|
||||||
await asset_service.purge_asset(user_id=current_user.id, asset_id=asset_id)
|
await asset_service.delete_asset(user_id=current_user.id, asset_id=asset_id)
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,6 @@ class AssetStatus(str, enum.Enum):
|
||||||
UPLOADING = "uploading"
|
UPLOADING = "uploading"
|
||||||
READY = "ready"
|
READY = "ready"
|
||||||
FAILED = "failed"
|
FAILED = "failed"
|
||||||
DELETED = "deleted"
|
|
||||||
|
|
||||||
|
|
||||||
class User(Base):
|
class User(Base):
|
||||||
|
|
@ -79,9 +78,6 @@ class Asset(Base):
|
||||||
created_at: Mapped[datetime] = mapped_column(
|
created_at: Mapped[datetime] = mapped_column(
|
||||||
DateTime(timezone=True), server_default=func.now(), nullable=False, index=True
|
DateTime(timezone=True), server_default=func.now(), nullable=False, index=True
|
||||||
)
|
)
|
||||||
deleted_at: Mapped[Optional[datetime]] = mapped_column(
|
|
||||||
DateTime(timezone=True), nullable=True, index=True
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class Share(Base):
|
class Share(Base):
|
||||||
|
|
|
||||||
|
|
@ -142,6 +142,27 @@ class S3Client:
|
||||||
except ClientError:
|
except ClientError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
def move_to_trash(self, storage_key: str) -> None:
|
||||||
|
"""
|
||||||
|
Move an object from media bucket to trash bucket.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
storage_key: S3 object key in media bucket
|
||||||
|
"""
|
||||||
|
trash_bucket = "itcloud-trash"
|
||||||
|
try:
|
||||||
|
# Copy object to trash bucket
|
||||||
|
self.client.copy_object(
|
||||||
|
Bucket=trash_bucket,
|
||||||
|
Key=storage_key,
|
||||||
|
CopySource={"Bucket": self.bucket, "Key": storage_key},
|
||||||
|
)
|
||||||
|
# Delete from media bucket
|
||||||
|
self.client.delete_object(Bucket=self.bucket, Key=storage_key)
|
||||||
|
except ClientError as e:
|
||||||
|
# If object doesn't exist, ignore
|
||||||
|
pass
|
||||||
|
|
||||||
async def stream_object(
|
async def stream_object(
|
||||||
self, storage_key: str, chunk_size: int = 8192
|
self, storage_key: str, chunk_size: int = 8192
|
||||||
) -> Tuple[AsyncIterator[bytes], int]:
|
) -> Tuple[AsyncIterator[bytes], int]:
|
||||||
|
|
|
||||||
|
|
@ -77,7 +77,6 @@ class AssetRepository:
|
||||||
limit: int = 50,
|
limit: int = 50,
|
||||||
cursor: Optional[str] = None,
|
cursor: Optional[str] = None,
|
||||||
asset_type: Optional[AssetType] = None,
|
asset_type: Optional[AssetType] = None,
|
||||||
include_deleted: bool = False,
|
|
||||||
) -> list[Asset]:
|
) -> list[Asset]:
|
||||||
"""
|
"""
|
||||||
List assets for a user.
|
List assets for a user.
|
||||||
|
|
@ -87,16 +86,12 @@ class AssetRepository:
|
||||||
limit: Maximum number of results
|
limit: Maximum number of results
|
||||||
cursor: Pagination cursor (asset_id)
|
cursor: Pagination cursor (asset_id)
|
||||||
asset_type: Filter by asset type
|
asset_type: Filter by asset type
|
||||||
include_deleted: Include soft-deleted assets
|
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
List of assets
|
List of assets
|
||||||
"""
|
"""
|
||||||
query = select(Asset).where(Asset.user_id == user_id)
|
query = select(Asset).where(Asset.user_id == user_id)
|
||||||
|
|
||||||
if not include_deleted:
|
|
||||||
query = query.where(Asset.deleted_at.is_(None))
|
|
||||||
|
|
||||||
if asset_type:
|
if asset_type:
|
||||||
query = query.where(Asset.type == asset_type)
|
query = query.where(Asset.type == asset_type)
|
||||||
|
|
||||||
|
|
@ -124,34 +119,6 @@ class AssetRepository:
|
||||||
await self.session.refresh(asset)
|
await self.session.refresh(asset)
|
||||||
return asset
|
return asset
|
||||||
|
|
||||||
async def soft_delete(self, asset: Asset) -> Asset:
|
|
||||||
"""
|
|
||||||
Soft delete an asset.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
asset: Asset to delete
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Updated asset
|
|
||||||
"""
|
|
||||||
asset.deleted_at = datetime.now(timezone.utc)
|
|
||||||
asset.status = AssetStatus.DELETED
|
|
||||||
return await self.update(asset)
|
|
||||||
|
|
||||||
async def restore(self, asset: Asset) -> Asset:
|
|
||||||
"""
|
|
||||||
Restore a soft-deleted asset.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
asset: Asset to restore
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Updated asset
|
|
||||||
"""
|
|
||||||
asset.deleted_at = None
|
|
||||||
asset.status = AssetStatus.READY
|
|
||||||
return await self.update(asset)
|
|
||||||
|
|
||||||
async def delete(self, asset: Asset) -> None:
|
async def delete(self, asset: Asset) -> None:
|
||||||
"""
|
"""
|
||||||
Permanently delete an asset.
|
Permanently delete an asset.
|
||||||
|
|
|
||||||
|
|
@ -315,71 +315,20 @@ class AssetService:
|
||||||
|
|
||||||
return file_stream, content_type, content_length
|
return file_stream, content_type, content_length
|
||||||
|
|
||||||
async def delete_asset(self, user_id: str, asset_id: str) -> Asset:
|
async def delete_asset(self, user_id: str, asset_id: str) -> None:
|
||||||
"""
|
"""
|
||||||
Soft delete an asset.
|
Delete an asset permanently (move files to trash bucket, delete from DB).
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
user_id: User ID
|
user_id: User ID
|
||||||
asset_id: Asset ID
|
asset_id: Asset ID
|
||||||
|
|
||||||
Returns:
|
|
||||||
Updated asset
|
|
||||||
"""
|
"""
|
||||||
asset = await self.get_asset(user_id, asset_id)
|
asset = await self.get_asset(user_id, asset_id)
|
||||||
return await self.asset_repo.soft_delete(asset)
|
|
||||||
|
|
||||||
async def restore_asset(self, user_id: str, asset_id: str) -> Asset:
|
# Move files to trash bucket in S3
|
||||||
"""
|
self.s3_client.move_to_trash(asset.storage_key_original)
|
||||||
Restore a soft-deleted asset.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
user_id: User ID
|
|
||||||
asset_id: Asset ID
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Updated asset
|
|
||||||
"""
|
|
||||||
asset = await self.asset_repo.get_by_id(asset_id)
|
|
||||||
if not asset or asset.user_id != user_id:
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=status.HTTP_404_NOT_FOUND,
|
|
||||||
detail="Asset not found",
|
|
||||||
)
|
|
||||||
|
|
||||||
if not asset.deleted_at:
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=status.HTTP_400_BAD_REQUEST,
|
|
||||||
detail="Asset is not deleted",
|
|
||||||
)
|
|
||||||
|
|
||||||
return await self.asset_repo.restore(asset)
|
|
||||||
|
|
||||||
async def purge_asset(self, user_id: str, asset_id: str) -> None:
|
|
||||||
"""
|
|
||||||
Permanently delete an asset.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
user_id: User ID
|
|
||||||
asset_id: Asset ID
|
|
||||||
"""
|
|
||||||
asset = await self.asset_repo.get_by_id(asset_id)
|
|
||||||
if not asset or asset.user_id != user_id:
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=status.HTTP_404_NOT_FOUND,
|
|
||||||
detail="Asset not found",
|
|
||||||
)
|
|
||||||
|
|
||||||
if not asset.deleted_at:
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=status.HTTP_400_BAD_REQUEST,
|
|
||||||
detail="Asset must be deleted before purging",
|
|
||||||
)
|
|
||||||
|
|
||||||
# Delete from S3
|
|
||||||
self.s3_client.delete_object(asset.storage_key_original)
|
|
||||||
if asset.storage_key_thumb:
|
if asset.storage_key_thumb:
|
||||||
self.s3_client.delete_object(asset.storage_key_thumb)
|
self.s3_client.move_to_trash(asset.storage_key_thumb)
|
||||||
|
|
||||||
# Delete from database
|
# Delete from database
|
||||||
await self.asset_repo.delete(asset)
|
await self.asset_repo.delete(asset)
|
||||||
|
|
|
||||||
|
|
@ -92,13 +92,16 @@ services:
|
||||||
- MINIO_ROOT_USER=${MINIO_ROOT_USER:-minioadmin}
|
- MINIO_ROOT_USER=${MINIO_ROOT_USER:-minioadmin}
|
||||||
- MINIO_ROOT_PASSWORD=${MINIO_ROOT_PASSWORD:-minioadmin}
|
- MINIO_ROOT_PASSWORD=${MINIO_ROOT_PASSWORD:-minioadmin}
|
||||||
- MEDIA_BUCKET=itcloud-media
|
- MEDIA_BUCKET=itcloud-media
|
||||||
|
- TRASH_BUCKET=itcloud-trash
|
||||||
entrypoint: >
|
entrypoint: >
|
||||||
/bin/sh -c "
|
/bin/sh -c "
|
||||||
set -e;
|
set -e;
|
||||||
mc alias set myminio http://minio:9000 $$MINIO_ROOT_USER $$MINIO_ROOT_PASSWORD;
|
mc alias set myminio http://minio:9000 $$MINIO_ROOT_USER $$MINIO_ROOT_PASSWORD;
|
||||||
mc mb myminio/$$MEDIA_BUCKET --ignore-existing;
|
mc mb myminio/$$MEDIA_BUCKET --ignore-existing;
|
||||||
|
mc mb myminio/$$TRASH_BUCKET --ignore-existing;
|
||||||
mc anonymous set none myminio/$$MEDIA_BUCKET;
|
mc anonymous set none myminio/$$MEDIA_BUCKET;
|
||||||
echo 'MinIO bucket ensured:' $$MEDIA_BUCKET;
|
mc anonymous set none myminio/$$TRASH_BUCKET;
|
||||||
|
echo 'MinIO buckets ensured:' $$MEDIA_BUCKET $$TRASH_BUCKET;
|
||||||
"
|
"
|
||||||
restart: "no"
|
restart: "no"
|
||||||
networks:
|
networks:
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,6 @@ import { Box } from '@mui/material';
|
||||||
import LoginPage from './pages/LoginPage';
|
import LoginPage from './pages/LoginPage';
|
||||||
import RegisterPage from './pages/RegisterPage';
|
import RegisterPage from './pages/RegisterPage';
|
||||||
import LibraryPage from './pages/LibraryPage';
|
import LibraryPage from './pages/LibraryPage';
|
||||||
import TrashPage from './pages/TrashPage';
|
|
||||||
import ShareViewPage from './pages/ShareViewPage';
|
import ShareViewPage from './pages/ShareViewPage';
|
||||||
import { useAuth } from './hooks/useAuth';
|
import { useAuth } from './hooks/useAuth';
|
||||||
|
|
||||||
|
|
@ -31,14 +30,6 @@ function App() {
|
||||||
</PrivateRoute>
|
</PrivateRoute>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<Route
|
|
||||||
path="/trash"
|
|
||||||
element={
|
|
||||||
<PrivateRoute>
|
|
||||||
<TrashPage />
|
|
||||||
</PrivateRoute>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<Route path="/" element={<Navigate to="/library" />} />
|
<Route path="/" element={<Navigate to="/library" />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,6 @@ import {
|
||||||
Menu as MenuIcon,
|
Menu as MenuIcon,
|
||||||
CloudUpload as CloudIcon,
|
CloudUpload as CloudIcon,
|
||||||
PhotoLibrary as LibraryIcon,
|
PhotoLibrary as LibraryIcon,
|
||||||
Delete as TrashIcon,
|
|
||||||
Logout as LogoutIcon,
|
Logout as LogoutIcon,
|
||||||
} from '@mui/icons-material';
|
} from '@mui/icons-material';
|
||||||
import { useAuth } from '../hooks/useAuth';
|
import { useAuth } from '../hooks/useAuth';
|
||||||
|
|
@ -56,7 +55,6 @@ export default function Layout({ children }: LayoutProps) {
|
||||||
|
|
||||||
const menuItems = [
|
const menuItems = [
|
||||||
{ text: 'Библиотека', icon: <LibraryIcon />, path: '/library' },
|
{ text: 'Библиотека', icon: <LibraryIcon />, path: '/library' },
|
||||||
{ text: 'Корзина', icon: <TrashIcon />, path: '/trash' },
|
|
||||||
];
|
];
|
||||||
|
|
||||||
const drawer = (
|
const drawer = (
|
||||||
|
|
|
||||||
|
|
@ -1,154 +0,0 @@
|
||||||
import { useState, useEffect } from 'react';
|
|
||||||
import {
|
|
||||||
Box,
|
|
||||||
Grid,
|
|
||||||
Typography,
|
|
||||||
CircularProgress,
|
|
||||||
Button,
|
|
||||||
Snackbar,
|
|
||||||
} from '@mui/material';
|
|
||||||
import Layout from '../components/Layout';
|
|
||||||
import MediaCard from '../components/MediaCard';
|
|
||||||
import type { Asset } from '../types';
|
|
||||||
import api from '../services/api';
|
|
||||||
|
|
||||||
export default function TrashPage() {
|
|
||||||
const [assets, setAssets] = useState<Asset[]>([]);
|
|
||||||
const [loading, setLoading] = useState(true);
|
|
||||||
const [selectedAssets, setSelectedAssets] = useState<Set<string>>(new Set());
|
|
||||||
const [snackbarOpen, setSnackbarOpen] = useState(false);
|
|
||||||
const [snackbarMessage, setSnackbarMessage] = useState('');
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
loadDeletedAssets();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const loadDeletedAssets = async () => {
|
|
||||||
try {
|
|
||||||
setLoading(true);
|
|
||||||
// We need to get all assets and filter deleted ones
|
|
||||||
// TODO: Add deleted filter to API
|
|
||||||
const response = await api.listAssets({ limit: 200 });
|
|
||||||
const deletedAssets = response.items.filter((asset) => asset.deleted_at);
|
|
||||||
setAssets(deletedAssets);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to load deleted assets:', error);
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSelect = (assetId: string, selected: boolean) => {
|
|
||||||
const newSelected = new Set(selectedAssets);
|
|
||||||
if (selected) {
|
|
||||||
newSelected.add(assetId);
|
|
||||||
} else {
|
|
||||||
newSelected.delete(assetId);
|
|
||||||
}
|
|
||||||
setSelectedAssets(newSelected);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleRestore = async () => {
|
|
||||||
if (selectedAssets.size === 0) return;
|
|
||||||
|
|
||||||
try {
|
|
||||||
await Promise.all(
|
|
||||||
Array.from(selectedAssets).map((assetId) => api.restoreAsset(assetId))
|
|
||||||
);
|
|
||||||
setAssets(assets.filter((a) => !selectedAssets.has(a.id)));
|
|
||||||
setSelectedAssets(new Set());
|
|
||||||
showSnackbar('Файлы восстановлены');
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to restore assets:', error);
|
|
||||||
showSnackbar('Ошибка при восстановлении файлов');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handlePurge = async () => {
|
|
||||||
if (selectedAssets.size === 0) return;
|
|
||||||
|
|
||||||
if (!confirm('Вы уверены? Файлы будут удалены навсегда.')) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
await Promise.all(
|
|
||||||
Array.from(selectedAssets).map((assetId) => api.purgeAsset(assetId))
|
|
||||||
);
|
|
||||||
setAssets(assets.filter((a) => !selectedAssets.has(a.id)));
|
|
||||||
setSelectedAssets(new Set());
|
|
||||||
showSnackbar('Файлы удалены навсегда');
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to purge assets:', error);
|
|
||||||
showSnackbar('Ошибка при удалении файлов');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const showSnackbar = (message: string) => {
|
|
||||||
setSnackbarMessage(message);
|
|
||||||
setSnackbarOpen(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Layout>
|
|
||||||
<Box sx={{ height: '100%', display: 'flex', flexDirection: 'column' }}>
|
|
||||||
{/* Actions */}
|
|
||||||
{selectedAssets.size > 0 && (
|
|
||||||
<Box sx={{ p: 2, borderBottom: 1, borderColor: 'divider', display: 'flex', gap: 2 }}>
|
|
||||||
<Typography variant="body1" sx={{ flexGrow: 1, alignSelf: 'center' }}>
|
|
||||||
Выбрано: {selectedAssets.size}
|
|
||||||
</Typography>
|
|
||||||
<Button variant="outlined" onClick={handleRestore}>
|
|
||||||
Восстановить
|
|
||||||
</Button>
|
|
||||||
<Button variant="outlined" color="error" onClick={handlePurge}>
|
|
||||||
Удалить навсегда
|
|
||||||
</Button>
|
|
||||||
</Box>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Content */}
|
|
||||||
<Box sx={{ flexGrow: 1, overflow: 'auto', p: 2 }}>
|
|
||||||
{loading && (
|
|
||||||
<Box sx={{ display: 'flex', justifyContent: 'center', p: 4 }}>
|
|
||||||
<CircularProgress />
|
|
||||||
</Box>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{!loading && assets.length === 0 && (
|
|
||||||
<Box sx={{ textAlign: 'center', p: 4 }}>
|
|
||||||
<Typography variant="h6" color="text.secondary" gutterBottom>
|
|
||||||
Корзина пуста
|
|
||||||
</Typography>
|
|
||||||
<Typography variant="body2" color="text.secondary">
|
|
||||||
Удаленные файлы будут отображаться здесь
|
|
||||||
</Typography>
|
|
||||||
</Box>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{assets.length > 0 && (
|
|
||||||
<Grid container spacing={2}>
|
|
||||||
{assets.map((asset) => (
|
|
||||||
<Grid item xs={6} sm={4} md={3} lg={2} key={asset.id}>
|
|
||||||
<MediaCard
|
|
||||||
asset={asset}
|
|
||||||
selected={selectedAssets.has(asset.id)}
|
|
||||||
onSelect={handleSelect}
|
|
||||||
/>
|
|
||||||
</Grid>
|
|
||||||
))}
|
|
||||||
</Grid>
|
|
||||||
)}
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
{/* Snackbar */}
|
|
||||||
<Snackbar
|
|
||||||
open={snackbarOpen}
|
|
||||||
autoHideDuration={3000}
|
|
||||||
onClose={() => setSnackbarOpen(false)}
|
|
||||||
message={snackbarMessage}
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
</Layout>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -102,18 +102,8 @@ class ApiClient {
|
||||||
return response.data;
|
return response.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
async deleteAsset(assetId: string): Promise<Asset> {
|
async deleteAsset(assetId: string): Promise<void> {
|
||||||
const { data } = await this.client.delete(`/assets/${assetId}`);
|
await this.client.delete(`/assets/${assetId}`);
|
||||||
return data;
|
|
||||||
}
|
|
||||||
|
|
||||||
async restoreAsset(assetId: string): Promise<Asset> {
|
|
||||||
const { data } = await this.client.post(`/assets/${assetId}/restore`);
|
|
||||||
return data;
|
|
||||||
}
|
|
||||||
|
|
||||||
async purgeAsset(assetId: string): Promise<void> {
|
|
||||||
await this.client.delete(`/assets/${assetId}/purge`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Upload
|
// Upload
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ export interface AuthTokens {
|
||||||
}
|
}
|
||||||
|
|
||||||
export type AssetType = 'photo' | 'video';
|
export type AssetType = 'photo' | 'video';
|
||||||
export type AssetStatus = 'uploading' | 'ready' | 'failed' | 'deleted';
|
export type AssetStatus = 'uploading' | 'ready' | 'failed';
|
||||||
|
|
||||||
export interface Asset {
|
export interface Asset {
|
||||||
id: string;
|
id: string;
|
||||||
|
|
@ -30,7 +30,6 @@ export interface Asset {
|
||||||
storage_key_original: string;
|
storage_key_original: string;
|
||||||
storage_key_thumb?: string;
|
storage_key_thumb?: string;
|
||||||
created_at: string;
|
created_at: string;
|
||||||
deleted_at?: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AssetListResponse {
|
export interface AssetListResponse {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue