sync: support quota of cloud storage (#261)

This commit is contained in:
Huang Xin
2025-01-28 19:05:20 +01:00
committed by GitHub
parent efd83180f1
commit 171a7ee759
29 changed files with 308 additions and 85 deletions
@@ -144,5 +144,7 @@
"Book downloaded: {{title}}": "Buch heruntergeladen: {{title}}",
"Failed to download book: {{title}}": "Fehler beim Herunterladen des Buches: {{title}}",
"Upload Book": "Buch hochladen",
"Auto Upload Books to Cloud": "Bücher automatisch in die Cloud hochladen"
"Auto Upload Books to Cloud": "Bücher automatisch in die Cloud hochladen",
"{{percentage}}% of Cloud Storage Used.": "{{percentage}}% des Cloud-Speichers verwendet.",
"Storage": "Speicher"
}
@@ -144,5 +144,7 @@
"Book downloaded: {{title}}": "Το βιβλίο με τίτλο {{title}} κατέβηκε",
"Failed to download book: {{title}}": "Αποτυχία λήψης βιβλίου: {{title}}",
"Upload Book": "Ανέβασμα βιβλίου",
"Auto Upload Books to Cloud": "Αυτόματο ανέβασμα βιβλίων στο cloud"
"Auto Upload Books to Cloud": "Αυτόματο ανέβασμα βιβλίων στο cloud",
"{{percentage}}% of Cloud Storage Used.": "{{percentage}}% της αποθήκευσης στο cloud χρησιμοποιήθηκε.",
"Storage": "Αποθήκευση"
}
@@ -144,5 +144,7 @@
"Book downloaded: {{title}}": "Libro descargado: {{title}}",
"Failed to download book: {{title}}": "Error al descargar libro: {{title}}",
"Upload Book": "Subir libro",
"Auto Upload Books to Cloud": "Subir libros automáticamente a la nube"
"Auto Upload Books to Cloud": "Subir libros automáticamente a la nube",
"{{percentage}}% of Cloud Storage Used.": "{{percentage}}% del almacenamiento en la nube utilizado.",
"Storage": "Almacenamiento"
}
@@ -144,5 +144,7 @@
"Book downloaded: {{title}}": "Livre téléchargé : {{title}}",
"Failed to download book: {{title}}": "Échec du téléchargement du livre : {{title}}",
"Upload Book": "Télécharger un livre",
"Auto Upload Books to Cloud": "Télécharger automatiquement les livres dans le cloud"
"Auto Upload Books to Cloud": "Télécharger automatiquement les livres dans le cloud",
"{{percentage}}% of Cloud Storage Used.": "{{percentage}}% de l'espace de stockage dans le cloud utilisé.",
"Storage": "Stockage"
}
@@ -144,5 +144,7 @@
"Book downloaded: {{title}}": "पुस्तक डाउनलोड की गई: {{title}}",
"Failed to download book: {{title}}": "पुस्तक डाउनलोड करने में विफल: {{title}}",
"Upload Book": "पुस्तक अपलोड करें",
"Auto Upload Books to Cloud": "पुस्तकें बादल में स्वचालित रूप से अपलोड करें"
"Auto Upload Books to Cloud": "पुस्तकें बादल में स्वचालित रूप से अपलोड करें",
"{{percentage}}% of Cloud Storage Used.": "{{percentage}}% क्लाउड स्टोरेज का उपयोग किया गया है।",
"Storage": "स्टोरेज"
}
@@ -144,5 +144,7 @@
"Book downloaded: {{title}}": "Buku diunduh: {{title}}",
"Failed to download book: {{title}}": "Gagal mengunduh buku: {{title}}",
"Upload Book": "Unggah Buku",
"Auto Upload Books to Cloud": "Unggah Buku Otomatis ke Cloud"
"Auto Upload Books to Cloud": "Unggah Buku Otomatis ke Cloud",
"{{percentage}}% of Cloud Storage Used.": "{{percentage}}% dari Penyimpanan Cloud Digunakan.",
"Storage": "Penyimpanan"
}
@@ -144,5 +144,7 @@
"Book downloaded: {{title}}": "Libro scaricato: {{title}}",
"Failed to download book: {{title}}": "Download libro non riuscito: {{title}}",
"Upload Book": "Carica libro",
"Auto Upload Books to Cloud": "Carica libri automaticamente su cloud"
"Auto Upload Books to Cloud": "Carica libri automaticamente su cloud",
"{{percentage}}% of Cloud Storage Used.": "{{percentage}}% dello spazio di archiviazione cloud utilizzato.",
"Storage": "Archiviazione"
}
@@ -144,5 +144,7 @@
"Book downloaded: {{title}}": "書籍がダウンロードされました:{{title}}",
"Failed to download book: {{title}}": "書籍のダウンロードに失敗しました:{{title}}",
"Upload Book": "書籍をアップロード",
"Auto Upload Books to Cloud": "書籍を自動的にクラウドにアップロード"
"Auto Upload Books to Cloud": "書籍を自動的にクラウドにアップロード",
"{{percentage}}% of Cloud Storage Used.": "クラウドストレージの{{percentage}}%が使用されています。",
"Storage": "ストレージ"
}
@@ -144,5 +144,7 @@
"Book downloaded: {{title}}": "책 다운로드됨: {{title}}",
"Failed to download book: {{title}}": "책 다운로드 실패: {{title}}",
"Upload Book": "책 업로드",
"Auto Upload Books to Cloud": "책 자동으로 클라우드에 업로드"
"Auto Upload Books to Cloud": "책 자동으로 클라우드에 업로드",
"{{percentage}}% of Cloud Storage Used.": "클라우드 저장소 사용량 {{percentage}}%.",
"Storage": "저장소"
}
@@ -144,5 +144,7 @@
"Book downloaded: {{title}}": "Książka pobrana: {{title}}",
"Failed to download book: {{title}}": "Nie udało się pobrać książki: {{title}}",
"Upload Book": "Prześlij książkę",
"Auto Upload Books to Cloud": "Automatycznie przesyłaj książki do chmury"
"Auto Upload Books to Cloud": "Automatycznie przesyłaj książki do chmury",
"{{percentage}}% of Cloud Storage Used.": "{{percentage}}% przestrzeni dyskowej w chmurze użytej.",
"Storage": "Przechowywanie"
}
@@ -144,5 +144,7 @@
"Book downloaded: {{title}}": "Livro baixado: {{title}}",
"Failed to download book: {{title}}": "Falha ao baixar livro: {{title}}",
"Upload Book": "Enviar Livro",
"Auto Upload Books to Cloud": "Enviar Livros para a Nuvem Automaticamente"
"Auto Upload Books to Cloud": "Enviar Livros para a Nuvem Automaticamente",
"{{percentage}}% of Cloud Storage Used.": "{{percentage}}% do Armazenamento na Nuvem Usado.",
"Storage": "Armazenamento"
}
@@ -144,5 +144,7 @@
"Book downloaded: {{title}}": "Книга скачана: {{title}}",
"Failed to download book: {{title}}": "Не удалось скачать книгу: {{title}}",
"Upload Book": "Загрузить книгу",
"Auto Upload Books to Cloud": "Автоматическая загрузка книг в облако"
"Auto Upload Books to Cloud": "Автоматическая загрузка книг в облако",
"{{percentage}}% of Cloud Storage Used.": "{{percentage}}% использовано облачного хранилища.",
"Storage": "Хранилище"
}
@@ -144,5 +144,7 @@
"Book downloaded: {{title}}": "Kitap indirildi: {{title}}",
"Failed to download book: {{title}}": "Kitap indirilemedi: {{title}}",
"Upload Book": "Kitap Yükle",
"Auto Upload Books to Cloud": "Kitapları Otomatik Olarak Buluta Yükle"
"Auto Upload Books to Cloud": "Kitapları Otomatik Olarak Buluta Yükle",
"{{percentage}}% of Cloud Storage Used.": "Bulut Depolamanın %{{percentage}}'si kullanıldı.",
"Storage": "Depolama"
}
@@ -144,5 +144,7 @@
"Book downloaded: {{title}}": "Книгу завантажено: {{title}}",
"Failed to download book: {{title}}": "Не вдалося завантажити книгу: {{title}}",
"Upload Book": "Завантажити книгу",
"Auto Upload Books to Cloud": "Автоматично завантажувати книги в хмару"
"Auto Upload Books to Cloud": "Автоматично завантажувати книги в хмару",
"{{percentage}}% of Cloud Storage Used.": "{{percentage}}% використано хмарного сховища.",
"Storage": "Сховище"
}
@@ -144,5 +144,7 @@
"Book downloaded: {{title}}": "Sách đã tải về: {{title}}",
"Failed to download book: {{title}}": "Không thể tải về sách: {{title}}",
"Upload Book": "Tải lên sách",
"Auto Upload Books to Cloud": "Tự động tải sách lên đám mây"
"Auto Upload Books to Cloud": "Tự động tải sách lên đám mây",
"{{percentage}}% of Cloud Storage Used.": "{{percentage}}% dung lượng lưu trữ đám mây đã sử dụng.",
"Storage": "Lưu trữ"
}
@@ -144,5 +144,7 @@
"Book downloaded: {{title}}": "书籍已下载:{{title}}",
"Failed to download book: {{title}}": "下载书籍失败:{{title}}",
"Upload Book": "上传书籍",
"Auto Upload Books to Cloud": "自动上传书籍到云端"
"Auto Upload Books to Cloud": "自动上传书籍到云端",
"{{percentage}}% of Cloud Storage Used.": "已使用 {{percentage}}% 云存储空间",
"Storage": "存储空间"
}
@@ -144,5 +144,7 @@
"Book downloaded: {{title}}": "書籍已下載:{{title}}",
"Failed to download book: {{title}}": "書籍下載失敗:{{title}}",
"Upload Book": "上傳書籍",
"Auto Upload Books to Cloud": "自動上傳書籍到雲端"
"Auto Upload Books to Cloud": "自動上傳書籍到雲端",
"{{percentage}}% of Cloud Storage Used.": "已使用 {{percentage}}% 雲存儲空間",
"Storage": "存儲空間"
}
@@ -29,6 +29,7 @@ const UNGROUPED_NAME = 'ungrouped';
const generateBookshelfItems = (books: Book[]): BookshelfItem[] => {
const groups: BooksGroup[] = books.reduce((acc: BooksGroup[], book: Book) => {
if (book.deletedAt) return acc;
book.group = book.group || UNGROUPED_NAME;
const groupIndex = acc.findIndex((group) => group.name === book.group);
const booksGroup = acc[acc.findIndex((group) => group.name === book.group)];
@@ -1,4 +1,4 @@
import React from 'react';
import React, { useEffect } from 'react';
import Image from 'next/image';
import { useRouter } from 'next/navigation';
import { PiUserCircle } from 'react-icons/pi';
@@ -11,7 +11,10 @@ import { useAuth } from '@/context/AuthContext';
import { useEnv } from '@/context/EnvContext';
import { useSettingsStore } from '@/store/settingsStore';
import { useTranslation } from '@/hooks/useTranslation';
import { getStoragePlanData } from '@/utils/access';
import { QuotaType } from '@/types/user';
import MenuItem from '@/components/MenuItem';
import Quota from '@/components/Quota';
interface BookMenuProps {
setIsDropdownOpen?: (isOpen: boolean) => void;
@@ -21,8 +24,9 @@ const SettingsMenu: React.FC<BookMenuProps> = ({ setIsDropdownOpen }) => {
const _ = useTranslation();
const router = useRouter();
const { envConfig } = useEnv();
const { user, logout } = useAuth();
const { token, user, logout } = useAuth();
const { settings, setSettings, saveSettings } = useSettingsStore();
const [quotas, setQuotas] = React.useState<QuotaType[]>([]);
const showAboutReadest = () => {
setAboutDialogVisible(true);
@@ -46,6 +50,22 @@ const SettingsMenu: React.FC<BookMenuProps> = ({ setIsDropdownOpen }) => {
setIsDropdownOpen?.(false);
};
useEffect(() => {
if (!user || !token) return;
const storagPlan = getStoragePlanData(token);
const storageQuota: QuotaType = {
name: _('Storage'),
tooltip: _('{{percentage}}% of Cloud Storage Used.', {
percentage: Math.round((storagPlan.usage / storagPlan.quota) * 100),
}),
used: Math.round(storagPlan.usage / 1024 / 1024),
total: Math.round(storagPlan.quota / 1024 / 1024),
unit: 'MB',
};
setQuotas([storageQuota]);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [token]);
const isWebApp = isWebAppPlatform();
const avatarUrl = user?.user_metadata?.['picture'] || user?.user_metadata?.['avatar_url'];
const userFullName = user?.user_metadata?.['full_name'];
@@ -54,7 +74,7 @@ const SettingsMenu: React.FC<BookMenuProps> = ({ setIsDropdownOpen }) => {
return (
<div
tabIndex={0}
className='settings-menu dropdown-content no-triangle border-base-100 z-20 mt-3 w-60 shadow-2xl'
className='settings-menu dropdown-content no-triangle border-base-100 z-20 mt-3 w-72 shadow-2xl'
>
{user ? (
<MenuItem
@@ -80,6 +100,7 @@ const SettingsMenu: React.FC<BookMenuProps> = ({ setIsDropdownOpen }) => {
}
>
<ul>
<Quota quotas={quotas} />
<MenuItem label={_('Sign Out')} noIcon onClick={handleUserLogout} />
</ul>
</MenuItem>
+35
View File
@@ -0,0 +1,35 @@
import React from 'react';
type QuotaProps = {
quotas: {
name: string;
tooltip: string;
used: number;
total: number;
unit: string;
}[];
};
const Quota: React.FC<QuotaProps> = ({ quotas }) => {
return (
<div className='w-full max-w-lg rounded-md pl-4 pr-2 text-base sm:text-sm'>
<div>
{quotas.map((quota) => (
<div key={quota.name} className='flex h-10 items-center justify-between'>
<div className='lg:tooltip lg:tooltip-bottom' data-tip={quota.tooltip}>
<div className='flex max-w-28 items-center gap-x-2'>
<span className='truncate'>{quota.name}</span>
</div>
</div>
<div className='py-3 text-right text-xs'>
{quota.used} / {quota.total} {quota.unit}
</div>
</div>
))}
</div>
</div>
);
};
export default Quota;
+51 -30
View File
@@ -1,19 +1,38 @@
import { getAPIBaseUrl } from '@/services/environment';
import { getAccessToken, getUserID } from '@/utils/access';
const UPLOAD_API_ENDPOINT = getAPIBaseUrl() + '/storage/upload';
const DOWNLOAD_API_ENDPOINT = getAPIBaseUrl() + '/storage/download';
const API_ENDPOINTS = {
upload: getAPIBaseUrl() + '/storage/upload',
download: getAPIBaseUrl() + '/storage/download',
delete: getAPIBaseUrl() + '/storage/delete',
};
export const uploadFile = async (file: File, bookHash?: string) => {
const fetchWithAuth = async (url: string, options: RequestInit) => {
const token = await getAccessToken();
if (!token) {
throw new Error('Not authenticated');
}
const headers = {
...options.headers,
Authorization: `Bearer ${token}`,
};
const response = await fetch(url, { ...options, headers });
if (!response.ok) {
const errorData = await response.json();
console.error('Error:', errorData.error || response.statusText);
throw new Error(errorData.error || 'Request failed');
}
return response;
};
export const uploadFile = async (file: File, bookHash?: string) => {
try {
const response = await fetch(UPLOAD_API_ENDPOINT, {
const response = await fetchWithAuth(API_ENDPOINTS.upload, {
method: 'POST',
headers: {
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
@@ -23,14 +42,8 @@ export const uploadFile = async (file: File, bookHash?: string) => {
}),
});
if (!response.ok) {
const errorData = await response.json();
console.log('Error generating presigned post:', errorData.error);
throw new Error('File upload failed');
}
const { uploadUrl } = await response.json();
const urlResponse = await response.json();
const { uploadUrl } = urlResponse;
const uploadResponse = await fetch(uploadUrl, {
method: 'PUT',
body: file,
@@ -46,30 +59,21 @@ export const uploadFile = async (file: File, bookHash?: string) => {
}
};
export const downloadFile = async (fp: string) => {
const token = await getAccessToken();
const userId = await getUserID();
if (!token || !userId) {
throw new Error('Not authenticated');
}
export const downloadFile = async (filePath: string) => {
try {
const fileKey = `${userId}/${fp}`;
const response = await fetch(
`${DOWNLOAD_API_ENDPOINT}?fileKey=${encodeURIComponent(fileKey)}`,
const userId = await getUserID();
if (!userId) {
throw new Error('Not authenticated');
}
const fileKey = `${userId}/${filePath}`;
const response = await fetchWithAuth(
`${API_ENDPOINTS.download}?fileKey=${encodeURIComponent(fileKey)}`,
{
method: 'GET',
headers: {
Authorization: `Bearer ${token}`,
},
},
);
if (!response.ok) {
const errorData = await response.json();
console.error('Error generating download URL:', errorData.error);
throw new Error('File download failed');
}
const { downloadUrl } = await response.json();
const downloadResponse = await fetch(downloadUrl);
@@ -84,3 +88,20 @@ export const downloadFile = async (fp: string) => {
throw new Error('File download failed');
}
};
export const deleteFile = async (filePath: string) => {
try {
const userId = await getUserID();
if (!userId) {
throw new Error('Not authenticated');
}
const fileKey = `${userId}/${filePath}`;
await fetchWithAuth(`${API_ENDPOINTS.delete}?fileKey=${encodeURIComponent(fileKey)}`, {
method: 'DELETE',
});
} catch (error) {
console.error('File deletion failed:', error);
throw new Error('File deletion failed');
}
};
@@ -0,0 +1,67 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import { corsAllMethods, runMiddleware } from '@/utils/cors';
import { createSupabaseClient } from '@/utils/supabase';
import { validateUserAndToken } from '@/utils/access';
import { DeleteObjectCommand } from '@aws-sdk/client-s3';
import { s3Client } from '@/utils/s3';
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
if (req.method !== 'DELETE') {
return res.status(405).json({ error: 'Method not allowed' });
}
await runMiddleware(req, res, corsAllMethods);
try {
const { user, token } = await validateUserAndToken(req.headers['authorization']);
if (!user || !token) {
return res.status(403).json({ error: 'Not authenticated' });
}
const { fileKey } = req.query;
if (!fileKey || typeof fileKey !== 'string') {
return res.status(400).json({ error: 'Missing or invalid fileKey' });
}
const supabase = createSupabaseClient(token);
const { data: fileRecord, error: fileError } = await supabase
.from('files')
.select('user_id, id')
.eq('user_id', user.id)
.eq('file_key', fileKey)
.limit(1)
.single();
if (fileError || !fileRecord) {
return res.status(404).json({ error: 'File not found' });
}
if (fileRecord.user_id !== user.id) {
return res.status(403).json({ error: 'Unauthorized access to the file' });
}
const deleteCommand = new DeleteObjectCommand({
Bucket: process.env['R2_BUCKET_NAME'] || '',
Key: fileKey,
});
try {
await s3Client.send(deleteCommand);
const { error: deleteError } = await supabase.from('files').delete().eq('id', fileRecord.id);
if (deleteError) {
console.error('Error updating file record:', deleteError);
return res.status(500).json({ error: 'Could not update file record' });
}
res.status(200).json({ message: 'File deleted successfully' });
} catch (error) {
console.error('Error deleting file from S3:', error);
res.status(500).json({ error: 'Could not delete file from storage' });
}
} catch (error) {
console.error(error);
return res.status(500).json({ error: 'Something went wrong' });
}
}
@@ -3,7 +3,7 @@ import { supabase, createSupabaseClient } from '@/utils/supabase';
import { corsAllMethods, runMiddleware } from '@/utils/cors';
import { PutObjectCommand } from '@aws-sdk/client-s3';
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
import { getPlanData } from '@/utils/access';
import { getStoragePlanData } from '@/utils/access';
import { s3Client } from '@/utils/s3';
const getUserAndToken = async (authHeader: string | undefined) => {
@@ -37,36 +37,52 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
return res.status(400).json({ error: 'Missing file info' });
}
const { usage, quota } = getPlanData(token);
const { usage, quota } = getStoragePlanData(token);
if (usage + fileSize > quota) {
return res.status(403).json({ error: 'Insufficient storage quota', usage });
}
const objectKey = `${user.id}/${fileName}`;
const supabase = createSupabaseClient(token);
const { data: existingRecord, error: fetchError } = await supabase
.from('files')
.select('*')
.eq('user_id', user.id)
.eq('file_key', objectKey)
.limit(1)
.single();
if (fetchError && fetchError.code !== 'PGRST116') {
return res.status(500).json({ error: fetchError.message });
}
let objSize = fileSize;
if (existingRecord) {
objSize = existingRecord.file_size;
} else {
const { data: inserted, error: insertError } = await supabase
.from('files')
.insert([
{
user_id: user.id,
book_hash: bookHash,
file_key: objectKey,
file_size: fileSize,
},
])
.select()
.single();
console.log('Inserted record:', inserted);
if (insertError) return res.status(500).json({ error: insertError.message });
}
const signableHeaders = new Set<string>();
signableHeaders.add('content-length');
const putCommand = new PutObjectCommand({
Bucket: process.env['R2_BUCKET_NAME'] || '',
Key: objectKey,
ContentLength: fileSize,
ContentLength: objSize,
});
const supabase = createSupabaseClient(token);
const { data: inserted, error: insertError } = await supabase
.from('files')
.insert([
{
user_id: user.id,
book_hash: bookHash,
file_key: objectKey,
file_size: fileSize,
},
])
.select()
.single();
console.log('Inserted record:', inserted);
if (insertError) return res.status(500).json({ error: insertError.message });
try {
const uploadUrl = await getSignedUrl(s3Client, putCommand, {
expiresIn: 1800,
+9 -4
View File
@@ -30,7 +30,7 @@ import {
} from './constants';
import { isValidURL } from '@/utils/misc';
import { deserializeConfig, serializeConfig } from '@/utils/serializer';
import { downloadFile, uploadFile } from '@/libs/storage';
import { downloadFile, uploadFile, deleteFile } from '@/libs/storage';
export abstract class BaseAppService implements AppService {
localBooksDir: string = '';
@@ -190,16 +190,20 @@ export abstract class BaseAppService implements AppService {
}
}
async deleteBook(book: Book): Promise<void> {
async deleteBook(book: Book, includingUploaded = false): Promise<void> {
for (const fp of [getFilename(book), getCoverFilename(book)]) {
if (await this.fs.exists(fp, 'Books')) {
await this.fs.removeFile(fp, 'Books');
}
if (includingUploaded) {
console.log('Deleting uploaded file:', fp);
const cfp = `${CLOUD_BOOKS_SUBDIR}/${fp}`;
await deleteFile(cfp);
}
}
}
async uploadBook(book: Book): Promise<void> {
console.log('Uploading book:', book.title);
let file: File;
let uploaded = false;
for (const fp of [getFilename(book), getCoverFilename(book)]) {
@@ -211,6 +215,7 @@ export abstract class BaseAppService implements AppService {
} else {
file = await new RemoteFile(this.fs.getURL(`${this.localBooksDir}/${fp}`), cfp).open();
}
console.log('Uploading file:', fp);
await uploadFile(file, book.hash);
uploaded = true;
}
@@ -223,7 +228,6 @@ export abstract class BaseAppService implements AppService {
}
async downloadBook(book: Book, onlyCover = false): Promise<void> {
console.log('Downloading book:', book.title);
const fps = [getCoverFilename(book)];
if (!onlyCover) {
fps.push(getFilename(book));
@@ -235,6 +239,7 @@ export abstract class BaseAppService implements AppService {
if (existed) {
downloaded = true;
} else {
console.log('Downloading file:', fp);
const cfp = `${CLOUD_BOOKS_SUBDIR}/${fp}`;
const fileobj = (await downloadFile(cfp)) as Blob;
await this.fs.writeFile(fp, 'Books', await fileobj.arrayBuffer());
+4 -2
View File
@@ -21,8 +21,10 @@ export const useLibraryStore = create<LibraryState>((set, get) => ({
const bookIndex = library.findIndex((b) => b.hash === book.hash);
if (bookIndex !== -1) {
if (isDelete) {
library.splice(bookIndex, 1);
appService.deleteBook(book);
appService.deleteBook(book, !!book.uploadedAt);
book.deletedAt = Date.now();
book.uploadedAt = null;
book.downloadedAt = null;
} else {
library[bookIndex] = book;
}
+1 -1
View File
@@ -38,7 +38,7 @@ export interface AppService {
saveCover?: boolean,
overwrite?: boolean,
): Promise<Book | null>;
deleteBook(book: Book): Promise<void>;
deleteBook(book: Book, includingUploaded?: boolean): Promise<void>;
uploadBook(book: Book): Promise<void>;
downloadBook(book: Book, onlyCover?: boolean): Promise<void>;
loadBookConfig(book: Book, settings: SystemSettings): Promise<BookConfig>;
+8
View File
@@ -5,3 +5,11 @@ export type UserStorageQuota = {
};
export type UserPlan = keyof UserStorageQuota;
export type QuotaType = {
name: string;
tooltip: string;
used: number;
total: number;
unit: string;
};
+14 -1
View File
@@ -10,7 +10,7 @@ interface Token {
[key: string]: string | number;
}
export const getPlanData = (token: string) => {
export const getStoragePlanData = (token: string) => {
const data = jwtDecode<Token>(token) || {};
const plan = data['plan'] || 'free';
const usage = data['storage_usage_bytes'] || 0;
@@ -42,3 +42,16 @@ export const getUserID = async (): Promise<string | null> => {
const { data } = await supabase.auth.getSession();
return data?.session?.user?.id ?? null;
};
export const validateUserAndToken = async (authHeader: string | undefined) => {
if (!authHeader) return {};
const token = authHeader.replace('Bearer ', '');
const {
data: { user },
error,
} = await supabase.auth.getUser(token);
if (error || !user) return {};
return { user, token };
};
+8 -8
View File
@@ -20,7 +20,7 @@ export const transformBookConfigToDB = (bookConfig: unknown, userId: string): DB
progress: progress && JSON.stringify(progress),
search_config: searchConfig && JSON.stringify(searchConfig),
view_settings: viewSettings && JSON.stringify(viewSettings),
updated_at: new Date(updatedAt).toISOString(),
updated_at: new Date(updatedAt ?? Date.now()).toISOString(),
};
};
@@ -60,8 +60,8 @@ export const transformBookToDB = (book: unknown, userId: string): DBBook => {
group,
tags: tags,
progress: progress,
created_at: new Date(createdAt).toISOString(),
updated_at: new Date(updatedAt).toISOString(),
created_at: new Date(createdAt ?? Date.now()).toISOString(),
updated_at: new Date(updatedAt ?? Date.now()).toISOString(),
deleted_at: deletedAt ? new Date(deletedAt).toISOString() : null,
uploaded_at: uploadedAt ? new Date(uploadedAt).toISOString() : null,
};
@@ -92,8 +92,8 @@ export const transformBookFromDB = (dbBook: DBBook): Book => {
progress: progress,
createdAt: new Date(created_at!).getTime(),
updatedAt: new Date(updated_at!).getTime(),
deletedAt: deleted_at ? new Date(deleted_at!).getTime() : null,
uploadedAt: uploaded_at ? new Date(uploaded_at!).getTime() : null,
deletedAt: deleted_at ? new Date(deleted_at).getTime() : null,
uploadedAt: uploaded_at ? new Date(uploaded_at).getTime() : null,
};
};
@@ -111,8 +111,8 @@ export const transformBookNoteToDB = (bookNote: unknown, userId: string): DBBook
style,
color,
note,
created_at: new Date(createdAt).toISOString(),
updated_at: new Date(updatedAt).toISOString(),
created_at: new Date(createdAt ?? Date.now()).toISOString(),
updated_at: new Date(updatedAt ?? Date.now()).toISOString(),
// note that only null deleted_at is updated to the database, undefined is not
deleted_at: deletedAt ? new Date(deletedAt).toISOString() : null,
};
@@ -133,6 +133,6 @@ export const transformBookNoteFromDB = (dbBookNote: DBBookNote): BookNote => {
note,
createdAt: new Date(created_at!).getTime(),
updatedAt: new Date(updated_at!).getTime(),
deletedAt: deleted_at ? new Date(deleted_at!).getTime() : null,
deletedAt: deleted_at ? new Date(deleted_at).getTime() : null,
};
};