diff --git a/docker-compose.yml b/docker-compose.yml
index 07e73c6..84da172 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -3,7 +3,7 @@ services:
postgres:
image: postgres
volumes:
- - pg-data:/var/lib/postgresql/data
+ - pg-data:/var/lib/postgresql
environment:
POSTGRES_USER: 'user'
POSTGRES_PASSWORD: 'pass'
@@ -18,6 +18,7 @@ services:
volumes:
- ./data:/data
environment:
+ KOMPANION_BSTORAGE_TYPE: postgres
KOMPANION_PG_URL: 'postgres://user:pass@postgres:5432/postgres'
KOMPANION_BSTORAGE_PATH: '/data/books/'
KOMPANION_AUTH_USERNAME: 'user'
@@ -28,4 +29,4 @@ services:
- postgres
volumes:
- pg-data:
+ pg-data:
\ No newline at end of file
diff --git a/internal/controller/http/web/books.go b/internal/controller/http/web/books.go
index e9aae9a..dfb1b17 100644
--- a/internal/controller/http/web/books.go
+++ b/internal/controller/http/web/books.go
@@ -29,6 +29,7 @@ func newBooksRoutes(handler *gin.RouterGroup, shelf library.Shelf, stats stats.R
handler.POST("/:bookID", r.updateBookMetadata)
handler.GET("/:bookID/download", r.downloadBook)
handler.GET("/:bookID/cover", r.viewBookCover)
+ handler.POST("/:bookID/delete", r.deleteBook)
}
func (r *booksRoutes) listBooks(c *gin.Context) {
@@ -203,3 +204,16 @@ func (r *booksRoutes) viewBookCover(c *gin.Context) {
}
c.File(cover.Name())
}
+
+func (r *booksRoutes) deleteBook(c *gin.Context) {
+ bookID := c.Param("bookID")
+
+ err := r.shelf.DeleteBook(c, bookID)
+ if err != nil {
+ r.logger.Error(err, "http - v1 - shelf - deleteBook")
+ c.JSON(500, passStandartContext(c, gin.H{"message": "internal server error"}))
+ return
+ }
+
+ c.Redirect(302, "/books")
+}
\ No newline at end of file
diff --git a/internal/library/book_postgres.go b/internal/library/book_postgres.go
index aa665d9..c3571e1 100644
--- a/internal/library/book_postgres.go
+++ b/internal/library/book_postgres.go
@@ -69,6 +69,27 @@ func (bdr *BookDatabaseRepo) Update(ctx context.Context, book entity.Book) error
return nil
}
+// Delete -. only delete from database
+func (bdr *BookDatabaseRepo) Delete(ctx context.Context, book entity.Book) error {
+ sql := `
+ DELETE FROM library_book
+ WHERE id = $1
+ `
+
+ args := []interface{}{
+ book.ID,
+ }
+
+ rows, err := bdr.Pool.Exec(ctx, sql, args...)
+ if err != nil {
+ return fmt.Errorf("BookDatabaseRepo - Delete - r.Pool.Exec: %w", err)
+ }
+ if rows.RowsAffected() == 0 {
+ return fmt.Errorf("BookDatabaseRepo - Delete - no rows affected")
+ }
+ return nil
+}
+
// List -. only select from database
func (bdr *BookDatabaseRepo) List(ctx context.Context,
sortBy, sortOrder string,
diff --git a/internal/library/interfaces.go b/internal/library/interfaces.go
index 52838ab..f7e85a1 100644
--- a/internal/library/interfaces.go
+++ b/internal/library/interfaces.go
@@ -19,6 +19,7 @@ type (
DownloadBook(ctx context.Context, bookID string) (entity.Book, *os.File, error)
UpdateBookMetadata(ctx context.Context, bookID string, metadata entity.Book) (entity.Book, error)
ViewCover(ctx context.Context, bookID string) (*os.File, error)
+ DeleteBook(ctx context.Context, bookID string) error
}
// BookRepo -.
@@ -32,5 +33,6 @@ type (
GetById(context.Context, string) (entity.Book, error)
GetByFileHash(context.Context, string) (entity.Book, error)
Update(context.Context, entity.Book) error
+ Delete(context.Context, entity.Book) error
}
)
diff --git a/internal/library/shelf.go b/internal/library/shelf.go
index b9bdff2..af02f89 100644
--- a/internal/library/shelf.go
+++ b/internal/library/shelf.go
@@ -145,6 +145,30 @@ func (uc *BookShelf) UpdateBookMetadata(ctx context.Context, bookID string, meta
return updatedBook, nil
}
+func (uc *BookShelf) DeleteBook(ctx context.Context, bookID string) error {
+ book, err := uc.repo.GetById(ctx, bookID)
+ if err != nil {
+ return fmt.Errorf("BookShelf - DeleteBook - s.repo.Get: %w", err)
+ }
+
+ err = uc.deleteCover(ctx, bookID)
+ if err != nil {
+ return fmt.Errorf("BookShelf - deleteCover - s.repo.Delete: %w", err)
+ }
+
+ err = uc.repo.Delete(ctx, book)
+ if err != nil {
+ return fmt.Errorf("BookShelf - DeleteBook - s.repo.Delete: %w", err)
+ }
+
+ err = uc.storage.Delete(ctx, book.FilePath)
+ if err != nil {
+ return fmt.Errorf("BookShelf - DeleteBook - s.storage.Delete: %w", err)
+ }
+
+ return nil
+}
+
func (uc *BookShelf) DownloadBook(ctx context.Context, bookID string) (entity.Book, *os.File, error) {
book, err := uc.repo.GetById(ctx, bookID)
if err != nil {
@@ -198,3 +222,18 @@ func writeCover(
}
return coverpath, nil
}
+
+func (uc *BookShelf) deleteCover(ctx context.Context, bookID string) error {
+ book, err := uc.repo.GetById(ctx, bookID)
+ if err != nil {
+ return fmt.Errorf("BookShelf - deleteCover - s.repo.Get: %w", err)
+ }
+ if book.CoverPath == "" {
+ return nil
+ }
+ err = uc.storage.Delete(ctx, book.CoverPath)
+ if err != nil {
+ return fmt.Errorf("BookShelf - deleteCover - s.Storage.Delete: %w", err)
+ }
+ return nil
+}
\ No newline at end of file
diff --git a/internal/storage/filesystem.go b/internal/storage/filesystem.go
index abde80a..5f6ecd6 100644
--- a/internal/storage/filesystem.go
+++ b/internal/storage/filesystem.go
@@ -75,6 +75,15 @@ func (s *FilesystemStorage) Write(ctx context.Context, src, dest string) error {
return nil
}
+func (s *FilesystemStorage) Delete(ctx context.Context, p string) error {
+ filepath := path.Join(s.root, p)
+ err := os.Remove(filepath)
+ if err != nil {
+ return err
+ }
+ return nil
+}
+
func checkSystemWrites(root string) error {
// Create a temporary file in the root directory
tempFile, err := os.CreateTemp(root, "write_test")
diff --git a/internal/storage/filesystem_test.go b/internal/storage/filesystem_test.go
index ac9b3f5..0baacef 100644
--- a/internal/storage/filesystem_test.go
+++ b/internal/storage/filesystem_test.go
@@ -42,6 +42,7 @@ func TestFilesystemStorage(t *testing.T) {
if err != nil {
t.Errorf("Error reading file: %v", err)
}
+
defer readFile.Close()
readBody, err := os.ReadFile(readFile.Name())
if err != nil {
@@ -50,4 +51,15 @@ func TestFilesystemStorage(t *testing.T) {
if string(readBody) != string(body) {
t.Errorf("Expected body %s, got %s", string(body), string(readBody))
}
+
+ err = st.Delete(ctx, "test")
+ if err != nil {
+ t.Errorf("Error deleting file: %v", err)
+ }
+
+ defer readFile.Close()
+ _, err =os.ReadFile(readFile.Name())
+ if err == nil {
+ t.Errorf("Error deleting file failed")
+ }
}
diff --git a/internal/storage/interfaces.go b/internal/storage/interfaces.go
index 0426bbe..515d866 100644
--- a/internal/storage/interfaces.go
+++ b/internal/storage/interfaces.go
@@ -8,4 +8,5 @@ import (
type Storage interface {
Write(ctx context.Context, source string, filepath string) error
Read(ctx context.Context, filepath string) (*os.File, error)
-}
+ Delete(ctx context.Context, filepath string) error
+}
\ No newline at end of file
diff --git a/internal/storage/memory.go b/internal/storage/memory.go
index 39499dd..815f539 100644
--- a/internal/storage/memory.go
+++ b/internal/storage/memory.go
@@ -53,3 +53,10 @@ func (s *MemoryStorage) Write(ctx context.Context, source string, filepath strin
s.mu.Unlock()
return nil
}
+
+func (s *MemoryStorage) Delete(ctx context.Context, filepath string) error {
+ s.mu.Lock()
+ s.data[filepath] = nil
+ s.mu.Unlock()
+ return nil
+}
\ No newline at end of file
diff --git a/internal/storage/postgres.go b/internal/storage/postgres.go
index 3ef33cd..56c1385 100644
--- a/internal/storage/postgres.go
+++ b/internal/storage/postgres.go
@@ -72,3 +72,17 @@ func (ps *PostgresStorage) Read(ctx context.Context, filepath string) (*os.File,
}
return tempFile, nil
}
+
+func (ps *PostgresStorage) Delete(ctx context.Context, filepath string) error {
+ sql := `
+ DELETE FROM storage_blob
+ WHERE file_path = $1
+ `
+ args := []interface{}{filepath}
+ _, err := ps.Pool.Exec(ctx, sql, args...)
+ if err != nil {
+ return fmt.Errorf("PostgresStorage - Read - r.Pool.QueryRow: %w", err)
+ }
+
+ return nil
+}
\ No newline at end of file
diff --git a/pkg/metadata/pdf.go b/pkg/metadata/pdf.go
index 38d8167..885d8b7 100644
--- a/pkg/metadata/pdf.go
+++ b/pkg/metadata/pdf.go
@@ -1,9 +1,14 @@
package metadata
import (
- "bufio"
"os"
+ "bytes"
+ "unicode/utf8"
+ "golang.org/x/text/encoding/unicode"
+ "golang.org/x/text/transform"
+ "regexp"
"strings"
+ "io"
)
// PDFMetadata holds the extracted PDFmetadata information
@@ -16,23 +21,35 @@ type PDFMetadata struct {
// extractPDFMetadataFromHeader scans the first part of the PDF file for PDFmetadata information
func extractPdfMetadata(tmpFile *os.File) (Metadata, error) {
- scanner := bufio.NewScanner(tmpFile)
var PDFmetadata Metadata
- for scanner.Scan() {
- line := scanner.Text()
- if strings.Contains(line, "/Title") {
- PDFmetadata.Title = extractValue(line, "/Title")
+ const chunksize = 64 * 1024
+ const tailsize = 1024
+
+ buf := make([]byte, chunksize)
+ tail := make([]byte, tailsize)
+
+ for {
+ n, err := tmpFile.Read(buf)
+
+ block := append(tail, buf[:n]...)
+
+ if bytes.Contains(block, []byte("/Title")) {
+ PDFmetadata.Title = clean(extractValue(block, "/Title"))
+
+ }
+
+ if bytes.Contains(block, []byte("/Author")) {
+ PDFmetadata.Author = clean(extractValue(block, "/Author"))
}
- if strings.Contains(line, "/Author") {
- PDFmetadata.Author = extractValue(line, "/Author")
+
+ if err == io.EOF {
+ break
+ }
+
+ if err != nil {
+ return Metadata{}, err
}
- // if strings.Contains(line, "/Subject") {
- // PDFmetadata.Subject = extractValue(line, "/Subject")
- // }
- // if strings.Contains(line, "/Keywords") {
- // PDFmetadata.Keywords = extractValue(line, "/Keywords")
- // }
// Break early if we've found all fields
if PDFmetadata.Title != "" && PDFmetadata.Author != "" {
@@ -40,23 +57,92 @@ func extractPdfMetadata(tmpFile *os.File) (Metadata, error) {
}
}
- if err := scanner.Err(); err != nil {
- return Metadata{}, err
+ // use file name if no title found
+ if PDFmetadata.Title == "" {
+ parts := strings.Split(tmpFile.Name(), "/")
+ parts = strings.Split(parts[len(parts) - 1], ".")
+ PDFmetadata.Title = parts[0]
}
return PDFmetadata, nil
}
// extractValue extracts the value for a specific metadata field
-func extractValue(line string, field string) string {
- start := strings.Index(line, field+"(")
- if start == -1 {
- return ""
+func extractValue(block []byte, field string) string {
+ fieldIndex := bytes.Index(block, []byte(field))
+ start := bytes.Index(block[fieldIndex:], []byte("(")) + fieldIndex + 1
+ end := findLiteralEnd(block[start:]) + start
+ value := block[start:end]
+
+ if utf8.Valid(value) {
+ return string(value)
+ }
+
+ // remove escape symbol
+ regex := regexp.MustCompile(`\\(.)`)
+ value = regex.ReplaceAll(value, []byte("$1"))
+
+ // UTF16BE
+ if len(value) >= 2 && value[0] == 0xFE && value[1] == 0xFF {
+ return decodeUTF16("be", value)
+ }
+ // UTF16LE
+ if len(value) >= 2 && value[0] == 0xFF && value[1] == 0xFE {
+ return decodeUTF16("le", value)
+ }
+
+ return ""
+}
+
+// find the literal end, excluding parentheis in the field
+func findLiteralEnd(b []byte) int {
+ depth := 1
+ escaped := false
+ for i := 0; i < len(b); i++ {
+ c := b[i]
+ if escaped {
+ escaped = false
+ continue
+ }
+ if c == '\\' {
+ escaped = true
+ continue
+ }
+ if c == '(' {
+ depth++
+ continue
+ }
+ if c == ')' {
+ depth--
+ if depth == 0 {
+ return i
+ }
+ }
}
- start += len(field) + 1 // Skip past the field and the opening parenthesis
- end := strings.Index(line[start:], ")")
- if end == -1 {
- return ""
+ return -1
+}
+
+// decode UTF-16 content
+func decodeUTF16(t string, data []byte) string {
+ var dec transform.Transformer
+ if t == "be" {
+ dec = unicode.UTF16(unicode.BigEndian, unicode.ExpectBOM).NewDecoder()
+ } else if t == "le" {
+ dec = unicode.UTF16(unicode.LittleEndian, unicode.ExpectBOM).NewDecoder()
}
- return line[start : start+end]
+
+ r := transform.NewReader(bytes.NewReader(data), dec)
+ b, _ := io.ReadAll(r)
+ return string(b)
}
+
+// remove non-utf8 bytes
+func clean(s string) string {
+ r := strings.NewReplacer(
+ "\x00", "",
+ "\xFF", "",
+ "\xFE", "",
+ )
+
+ return r.Replace(s)
+}
\ No newline at end of file
diff --git a/pkg/metadata/pdf_test.go b/pkg/metadata/pdf_test.go
new file mode 100644
index 0000000..76bdf43
--- /dev/null
+++ b/pkg/metadata/pdf_test.go
@@ -0,0 +1,48 @@
+package metadata
+
+import (
+ "os"
+ "path/filepath"
+ "testing"
+)
+
+
+func TestReadAllPDFBooks(t *testing.T) {
+ booksDir := "../../test/test_data/books"
+
+ err := filepath.Walk(booksDir, func(path string, info os.FileInfo, err error) error {
+ if err != nil {
+ return err
+ }
+
+ if info.IsDir() {
+ return nil
+ }
+
+ if filepath.Ext(path) == ".pdf" {
+ t.Logf("\nProcessing file: %s\n", path)
+
+ file, err := os.Open(path)
+ if err != nil {
+ t.Logf(" Error: failed to open file - %v\n", err)
+ return nil
+ }
+ defer file.Close()
+
+ metadata, err := extractPdfMetadata(file)
+ if err != nil {
+ t.Logf(" Error: failed to parse metadata - %v\n", err)
+ return nil
+ }
+
+ t.Logf(" Title: %s\n", metadata.Title)
+ t.Logf(" Author: %s\n", metadata.Author)
+ }
+
+ return nil
+ })
+
+ if err != nil {
+ t.Fatalf("Failed to traverse directory: %v", err)
+ }
+}
\ No newline at end of file
diff --git a/test/test_data/books/PrincessOfMars-PDF.pdf b/test/test_data/books/PrincessOfMars-PDF.pdf
index 0d1308d..eff9e65 100644
Binary files a/test/test_data/books/PrincessOfMars-PDF.pdf and b/test/test_data/books/PrincessOfMars-PDF.pdf differ
diff --git "a/test/test_data/books/\344\270\203\346\227\245\347\213\202\351\262\250.pdf" "b/test/test_data/books/\344\270\203\346\227\245\347\213\202\351\262\250.pdf"
new file mode 100644
index 0000000..49ac8e8
Binary files /dev/null and "b/test/test_data/books/\344\270\203\346\227\245\347\213\202\351\262\250.pdf" differ
diff --git "a/test/test_data/books/\345\220\264\346\260\217\347\237\263\345\244\264\350\256\260\345\242\236\345\210\240\350\257\225\350\257\204\346\234\254\357\274\210\347\231\270\351\205\211\346\234\254\357\274\211.pdf" "b/test/test_data/books/\345\220\264\346\260\217\347\237\263\345\244\264\350\256\260\345\242\236\345\210\240\350\257\225\350\257\204\346\234\254\357\274\210\347\231\270\351\205\211\346\234\254\357\274\211.pdf"
new file mode 100644
index 0000000..ada874f
Binary files /dev/null and "b/test/test_data/books/\345\220\264\346\260\217\347\237\263\345\244\264\350\256\260\345\242\236\345\210\240\350\257\225\350\257\204\346\234\254\357\274\210\347\231\270\351\205\211\346\234\254\357\274\211.pdf" differ
diff --git "a/test/test_data/books/\350\257\227\347\273\217\350\257\221\346\263\250.pdf" "b/test/test_data/books/\350\257\227\347\273\217\350\257\221\346\263\250.pdf"
new file mode 100644
index 0000000..315aed9
Binary files /dev/null and "b/test/test_data/books/\350\257\227\347\273\217\350\257\221\346\263\250.pdf" differ
diff --git a/web/templates/book.html b/web/templates/book.html
index 629d06e..c0c46ba 100644
--- a/web/templates/book.html
+++ b/web/templates/book.html
@@ -42,10 +42,26 @@
+
+
{{ end }}
{{ with $.stats }}