diff --git a/internal/core/ebook.go b/internal/core/ebook.go index 522094185..6fd45464d 100644 --- a/internal/core/ebook.go +++ b/internal/core/ebook.go @@ -16,7 +16,10 @@ import ( "github.com/pkg/errors" ) -func GenerateEbook(req ProcessRequest) (book model.Bookmark, err error) { +// GenerateEbook receives a `ProcessRequest` and generates an ebook file in the destination path specified. +// The destination path `dstPath` should include file name with ".epub" extension +// The bookmark model will be used to update the UI based on whether this function is successful or not. +func GenerateEbook(req ProcessRequest, dstPath string) (book model.Bookmark, err error) { // variable for store generated html code var html string @@ -27,6 +30,7 @@ func GenerateEbook(req ProcessRequest) (book model.Bookmark, err error) { return book, errors.New("bookmark ID is not valid") } + // get current state of bookmark // cheak archive and thumb strID := strconv.Itoa(book.ID) @@ -40,35 +44,23 @@ func GenerateEbook(req ProcessRequest) (book model.Bookmark, err error) { if _, err := os.Stat(archivePath); err == nil { book.HasArchive = true } - ebookfile := fp.Join(req.DataDir, "ebook", fmt.Sprintf("%d.epub", book.ID)) - // if epub exist finish prosess else continue - if _, err := os.Stat(ebookfile); err == nil { - book.HasEbook = true - return book, nil - } + + // this function create ebook from reader mode of bookmark so + // we can't create ebook from PDF so we return error here if bookmark is a pdf contentType := req.ContentType if strings.Contains(contentType, "application/pdf") { return book, errors.New("can't create ebook for pdf") } - ebookDir := fp.Join(req.DataDir, "ebook") - // check if directory not exsist create that - if _, err := os.Stat(ebookDir); os.IsNotExist(err) { - err := os.MkdirAll(ebookDir, model.DataDirPerm) - if err != nil { - return book, errors.Wrap(err, "can't create ebook directory") - } - } - // create epub file - epubFile, err := os.Create(ebookfile) + // create temporary epub file + tmpFile, err := os.CreateTemp("", "ebook") if err != nil { - return book, errors.Wrap(err, "can't create ebook") + return book, errors.Wrap(err, "can't create temporary EPUB file") } - defer epubFile.Close() + defer os.Remove(tmpFile.Name()) // Create zip archive - epubWriter := zip.NewWriter(epubFile) - defer epubWriter.Close() + epubWriter := zip.NewWriter(tmpFile) // Create the mimetype file mimetypeWriter, err := epubWriter.Create("mimetype") @@ -223,6 +215,27 @@ img { if err != nil { return book, errors.Wrap(err, "can't write into content.html") } + // close epub and tmpFile + err = epubWriter.Close() + if err != nil { + return book, errors.Wrap(err, "failed to close EPUB writer") + } + err = tmpFile.Close() + if err != nil { + return book, errors.Wrap(err, "failed to close temporary EPUB file") + } + // open temporary file again + tmpFile, err = os.Open(tmpFile.Name()) + if err != nil { + return book, errors.Wrap(err, "can't open temporary EPUB file") + } + defer tmpFile.Close() + // if everitings go well we start move ebook to dstPath + err = MoveFileToDestination(dstPath, tmpFile) + if err != nil { + return book, errors.Wrap(err, "failed move ebook to destination") + } + book.HasEbook = true return book, nil } diff --git a/internal/core/ebook_test.go b/internal/core/ebook_test.go index 77c7bf15f..25b0eca0d 100644 --- a/internal/core/ebook_test.go +++ b/internal/core/ebook_test.go @@ -1,7 +1,6 @@ package core_test import ( - "errors" "fmt" "os" fp "path/filepath" @@ -12,208 +11,165 @@ import ( "github.com/stretchr/testify/assert" ) -func TestGenerateEbook_ValidBookmarkID_ReturnsBookmarkWithHasEbookTrue(t *testing.T) { - tempDir := t.TempDir() - - defer os.RemoveAll(tempDir) - - mockRequest := core.ProcessRequest{ - Bookmark: model.Bookmark{ - ID: 1, - Title: "Example Bookmark", - HTML: "Example HTML", - HasEbook: false, - }, - DataDir: tempDir, - ContentType: "text/html", - } - - bookmark, err := core.GenerateEbook(mockRequest) - - assert.True(t, bookmark.HasEbook) - assert.NoError(t, err) -} - -func TestGenerateEbook_InvalidBookmarkID_ReturnsError(t *testing.T) { - tempDir := t.TempDir() - defer os.RemoveAll(tempDir) - mockRequest := core.ProcessRequest{ - Bookmark: model.Bookmark{ - ID: 0, - HasEbook: false, - }, - DataDir: tempDir, - ContentType: "text/html", - } - - bookmark, err := core.GenerateEbook(mockRequest) - - assert.Equal(t, model.Bookmark{ - ID: 0, - HasEbook: false, - }, bookmark) - assert.Error(t, err) -} - -func TestGenerateEbook_ValidBookmarkID_EbookExist_EbookExist_ReturnWithHasEbookTrue(t *testing.T) { - tempDir := t.TempDir() - defer os.RemoveAll(tempDir) - - mockRequest := core.ProcessRequest{ - Bookmark: model.Bookmark{ - ID: 1, - HasEbook: false, - }, - DataDir: tempDir, - ContentType: "text/html", - } - // Create the ebook directory - ebookDir := fp.Join(mockRequest.DataDir, "ebook") - err := os.MkdirAll(ebookDir, os.ModePerm) - if err != nil { - t.Fatal(err) - } - // Create the ebook file - ebookfile := fp.Join(mockRequest.DataDir, "ebook", fmt.Sprintf("%d.epub", mockRequest.Bookmark.ID)) - file, err := os.Create(ebookfile) - if err != nil { - t.Fatal(err) - } - defer file.Close() - - bookmark, err := core.GenerateEbook(mockRequest) - - assert.True(t, bookmark.HasEbook) - assert.NoError(t, err) -} - -func TestGenerateEbook_ValidBookmarkID_EbookExist_ImagePathExist_ReturnWithHasEbookTrue(t *testing.T) { - tempDir := t.TempDir() - defer os.RemoveAll(tempDir) - - mockRequest := core.ProcessRequest{ - Bookmark: model.Bookmark{ - ID: 1, - HasEbook: false, - }, - DataDir: tempDir, - ContentType: "text/html", - } - // Create the image directory - imageDir := fp.Join(mockRequest.DataDir, "thumb") - err := os.MkdirAll(imageDir, os.ModePerm) - if err != nil { - t.Fatal(err) - } - // Create the image file - imagePath := fp.Join(mockRequest.DataDir, "thumb", fmt.Sprintf("%d", mockRequest.Bookmark.ID)) - file, err := os.Create(imagePath) - if err != nil { - t.Fatal(err) - } - defer file.Close() - - bookmark, err := core.GenerateEbook(mockRequest) - expectedimagePath := "/bookmark/1/thumb" - if expectedimagePath != bookmark.ImageURL { - t.Errorf("Expected imageURL %s, but got %s", bookmark.ImageURL, expectedimagePath) - } - assert.True(t, bookmark.HasEbook) - assert.NoError(t, err) -} - -func TestGenerateEbook_ValidBookmarkID_EbookExist_ReturnWithHasArchiveTrue(t *testing.T) { - tempDir := t.TempDir() - defer os.RemoveAll(tempDir) - - mockRequest := core.ProcessRequest{ - Bookmark: model.Bookmark{ - ID: 1, - HasEbook: false, - }, - DataDir: tempDir, - ContentType: "text/html", - } - // Create the archive directory - archiveDir := fp.Join(mockRequest.DataDir, "archive") - err := os.MkdirAll(archiveDir, os.ModePerm) - if err != nil { - t.Fatal(err) - } - // Create the archive file - archivePath := fp.Join(mockRequest.DataDir, "archive", fmt.Sprintf("%d", mockRequest.Bookmark.ID)) - file, err := os.Create(archivePath) - if err != nil { - t.Fatal(err) - } - defer file.Close() - - bookmark, err := core.GenerateEbook(mockRequest) - assert.True(t, bookmark.HasArchive) - assert.NoError(t, err) -} - -func TestGenerateEbook_ValidBookmarkID_RetuenError_PDF(t *testing.T) { - tempDir := t.TempDir() - defer os.RemoveAll(tempDir) - - mockRequest := core.ProcessRequest{ - Bookmark: model.Bookmark{ - ID: 1, - HasEbook: false, - }, - DataDir: tempDir, - ContentType: "application/pdf", - } - - bookmark, err := core.GenerateEbook(mockRequest) - - assert.False(t, bookmark.HasEbook) - assert.Error(t, err) - assert.Contains(t, err.Error(), "can't create ebook for pdf") -} - -func TestGenerateEbook_CreateEbookDirectoryNotWritable(t *testing.T) { - // Create a temporary directory to use as the parent directory - parentDir := t.TempDir() - - // Create a child directory with read-only permissions - ebookDir := fp.Join(parentDir, "ebook") - err := os.Mkdir(ebookDir, 0444) - if err != nil { - t.Fatalf("could not create ebook directory: %s", err) - } - - mockRequest := core.ProcessRequest{ - Bookmark: model.Bookmark{ - ID: 1, - HasEbook: false, - }, - DataDir: ebookDir, - ContentType: "text/html", - } - - // Call GenerateEbook to create the ebook directory - bookmark, err := core.GenerateEbook(mockRequest) - if err == nil { - t.Fatal("GenerateEbook succeeded even though MkdirAll should have failed") - } - if !errors.Is(err, os.ErrPermission) { - t.Fatalf("unexpected error: expected os.ErrPermission, got %v", err) - } - - // Check if the ebook directory still exists and has read-only permissions - info, err := os.Stat(ebookDir) - if err != nil { - t.Fatalf("could not retrieve ebook directory info: %s", err) - } - if !info.IsDir() { - t.Errorf("ebook directory is not a directory") - } - if info.Mode().Perm() != 0444 { - t.Errorf("ebook directory has incorrect permissions: expected 0444, got %o", info.Mode().Perm()) - } - assert.False(t, bookmark.HasEbook) +func TestGenerateEbook(t *testing.T) { + t.Run("Successful ebook generate", func(t *testing.T) { + t.Run("valid bookmarkId that return HasEbook true", func(t *testing.T) { + // test cae + tempDir := t.TempDir() + dstDir := t.TempDir() + + mockRequest := core.ProcessRequest{ + Bookmark: model.Bookmark{ + ID: 1, + Title: "Example Bookmark", + HTML: "Example HTML", + HasEbook: false, + }, + DataDir: dstDir, + ContentType: "text/html", + } + + bookmark, err := core.GenerateEbook(mockRequest, fp.Join(tempDir, "1")) + + assert.True(t, bookmark.HasEbook) + assert.NoError(t, err) + }) + t.Run("ebook generate with valid BookmarkID EbookExist ImagePathExist ReturnWithHasEbookTrue", func(t *testing.T) { + tempDir := t.TempDir() + dstDir := t.TempDir() + + mockRequest := core.ProcessRequest{ + Bookmark: model.Bookmark{ + ID: 1, + HasEbook: false, + }, + DataDir: dstDir, + ContentType: "text/html", + } + // Create the image directory + imageDir := fp.Join(mockRequest.DataDir, "thumb") + err := os.MkdirAll(imageDir, os.ModePerm) + if err != nil { + t.Fatal(err) + } + // Create the image file + imagePath := fp.Join(mockRequest.DataDir, "thumb", fmt.Sprintf("%d", mockRequest.Bookmark.ID)) + file, err := os.Create(imagePath) + if err != nil { + t.Fatal(err) + } + defer file.Close() + + bookmark, err := core.GenerateEbook(mockRequest, fp.Join(tempDir, "1")) + expectedimagePath := "/bookmark/1/thumb" + if expectedimagePath != bookmark.ImageURL { + t.Errorf("Expected imageURL %s, but got %s", bookmark.ImageURL, expectedimagePath) + } + assert.True(t, bookmark.HasEbook) + assert.NoError(t, err) + }) + t.Run("generate ebook valid BookmarkID EbookExist Returnh HasArchive True", func(t *testing.T) { + + tempDir := t.TempDir() + dstDir := t.TempDir() + + mockRequest := core.ProcessRequest{ + Bookmark: model.Bookmark{ + ID: 1, + HasEbook: false, + }, + DataDir: dstDir, + ContentType: "text/html", + } + // Create the archive directory + archiveDir := fp.Join(mockRequest.DataDir, "archive") + err := os.MkdirAll(archiveDir, os.ModePerm) + if err != nil { + t.Fatal(err) + } + // Create the archive file + archivePath := fp.Join(mockRequest.DataDir, "archive", fmt.Sprintf("%d", mockRequest.Bookmark.ID)) + file, err := os.Create(archivePath) + if err != nil { + t.Fatal(err) + } + defer file.Close() + + bookmark, err := core.GenerateEbook(mockRequest, fp.Join(tempDir, "1")) + assert.True(t, bookmark.HasArchive) + assert.NoError(t, err) + }) + }) + t.Run("specific ebook generate case", func(t *testing.T) { + t.Run("unvalid bookmarkId that return Error", func(t *testing.T) { + tempDir := t.TempDir() + mockRequest := core.ProcessRequest{ + Bookmark: model.Bookmark{ + ID: 0, + HasEbook: false, + }, + DataDir: tempDir, + ContentType: "text/html", + } + + bookmark, err := core.GenerateEbook(mockRequest, tempDir) + + assert.Equal(t, model.Bookmark{ + ID: 0, + HasEbook: false, + }, bookmark) + assert.Error(t, err) + }) + t.Run("ebook exist return HasEbook true", func(t *testing.T) { + tempDir := t.TempDir() + dstDir := t.TempDir() + + mockRequest := core.ProcessRequest{ + Bookmark: model.Bookmark{ + ID: 1, + HasEbook: false, + }, + DataDir: dstDir, + ContentType: "text/html", + } + // Create the ebook directory + ebookDir := fp.Join(mockRequest.DataDir, "ebook") + err := os.MkdirAll(ebookDir, os.ModePerm) + if err != nil { + t.Fatal(err) + } + // Create the ebook file + ebookfile := fp.Join(mockRequest.DataDir, "ebook", fmt.Sprintf("%d.epub", mockRequest.Bookmark.ID)) + file, err := os.Create(ebookfile) + if err != nil { + t.Fatal(err) + } + defer file.Close() + + bookmark, err := core.GenerateEbook(mockRequest, fp.Join(tempDir, "1")) + + assert.True(t, bookmark.HasEbook) + assert.NoError(t, err) + }) + t.Run("generate ebook valid BookmarkID RetuenError for PDF file", func(t *testing.T) { + tempDir := t.TempDir() + + mockRequest := core.ProcessRequest{ + Bookmark: model.Bookmark{ + ID: 1, + HasEbook: false, + }, + DataDir: tempDir, + ContentType: "application/pdf", + } + + bookmark, err := core.GenerateEbook(mockRequest, tempDir) + + assert.False(t, bookmark.HasEbook) + assert.Error(t, err) + assert.Contains(t, err.Error(), "can't create ebook for pdf") + }) + }) } // Add more unit tests for other scenarios that missing specialy diff --git a/internal/core/processing.go b/internal/core/processing.go index 12a2c1050..25df3d316 100644 --- a/internal/core/processing.go +++ b/internal/core/processing.go @@ -8,10 +8,10 @@ import ( "image/draw" "image/jpeg" "io" + "log" "math" "net/url" "os" - "path" fp "path/filepath" "strconv" "strings" @@ -37,6 +37,8 @@ type ProcessRequest struct { LogArchival bool } +var ErrNoSupportedImageType = errors.New("unsupported image type") + // ProcessBookmark process the bookmark and archive it if needed. // Return three values, is error fatal, and error value. func ProcessBookmark(req ProcessRequest) (book model.Bookmark, isFatalErr bool, err error) { @@ -66,13 +68,15 @@ func ProcessBookmark(req ProcessRequest) (book model.Bookmark, isFatalErr bool, } // If this is HTML, parse for readable content + strID := strconv.Itoa(book.ID) + imgPath := fp.Join(req.DataDir, "thumb", strID) var imageURLs []string if strings.Contains(contentType, "text/html") { isReadable := readability.Check(readabilityCheckInput) nurl, err := url.Parse(book.URL) if err != nil { - return book, true, fmt.Errorf("Failed to parse url: %v", err) + return book, true, fmt.Errorf("failed to parse url: %v", err) } article, err := readability.FromReader(readabilityInput, nurl) @@ -101,6 +105,8 @@ func ProcessBookmark(req ProcessRequest) (book model.Bookmark, isFatalErr bool, // Get image URL if article.Image != "" { imageURLs = append(imageURLs, article.Image) + } else { + os.Remove(imgPath) } if article.Favicon != "" { @@ -115,26 +121,32 @@ func ProcessBookmark(req ProcessRequest) (book model.Bookmark, isFatalErr bool, } // Save article image to local disk - strID := strconv.Itoa(book.ID) - imgPath := fp.Join(req.DataDir, "thumb", strID) - - for _, imageURL := range imageURLs { - err = downloadBookImage(imageURL, imgPath) + for i, imageURL := range imageURLs { + err = DownloadBookImage(imageURL, imgPath) + if err != nil && errors.Is(err, ErrNoSupportedImageType) { + log.Printf("%s: %s", err, imageURL) + if i == len(imageURLs)-1 { + os.Remove(imgPath) + } + } + if err != nil { + log.Printf("File download not successful for image URL: %s", imageURL) + continue + } if err == nil { - book.ImageURL = path.Join("/", "bookmark", strID, "thumb") + book.ImageURL = fp.Join("/", "bookmark", strID, "thumb") break } } // If needed, create ebook as well if book.CreateEbook { - ebookPath := fp.Join(req.DataDir, "ebook", fmt.Sprintf("%d.epub", book.ID)) - os.Remove(ebookPath) + ebookPath := fp.Join(req.DataDir, "ebook", strID+".epub") if strings.Contains(contentType, "application/pdf") { return book, false, errors.Wrap(err, "can't create ebook from pdf") } else { - _, err = GenerateEbook(req) + _, err = GenerateEbook(req, ebookPath) if err != nil { return book, true, errors.Wrap(err, "failed to create ebook") } @@ -144,8 +156,11 @@ func ProcessBookmark(req ProcessRequest) (book model.Bookmark, isFatalErr bool, // If needed, create offline archive as well if book.CreateArchive { - archivePath := fp.Join(req.DataDir, "archive", fmt.Sprintf("%d", book.ID)) - os.Remove(archivePath) + tmpFile, err := os.CreateTemp("", "archive") + if err != nil { + return book, false, fmt.Errorf("failed to create temp archive: %v", err) + } + defer os.Remove(tmpFile.Name()) archivalRequest := warc.ArchivalRequest{ URL: book.URL, @@ -155,18 +170,27 @@ func ProcessBookmark(req ProcessRequest) (book model.Bookmark, isFatalErr bool, LogEnabled: req.LogArchival, } - err = warc.NewArchive(archivalRequest, archivePath) + err = warc.NewArchive(archivalRequest, tmpFile.Name()) if err != nil { + defer os.Remove(tmpFile.Name()) return book, false, fmt.Errorf("failed to create archive: %v", err) } + // Prepare destination file. + dstPath := fp.Join(req.DataDir, "archive", fmt.Sprintf("%d", book.ID)) + + err = MoveFileToDestination(dstPath, tmpFile) + if err != nil { + return book, false, fmt.Errorf("failed move archive to destination `: %v", err) + } + book.HasArchive = true } return book, false, nil } -func downloadBookImage(url, dstPath string) error { +func DownloadBookImage(url, dstPath string) error { // Fetch data from URL resp, err := httpClient.Get(url) if err != nil { @@ -180,22 +204,16 @@ func downloadBookImage(url, dstPath string) error { !strings.Contains(cp, "image/pjpeg") && !strings.Contains(cp, "image/jpg") && !strings.Contains(cp, "image/png") { - - return fmt.Errorf("%s is not a supported image", url) + return ErrNoSupportedImageType } // At this point, the download has finished successfully. - // Prepare destination file. - err = os.MkdirAll(fp.Dir(dstPath), model.DataDirPerm) + // Create tmpFile + tmpFile, err := os.CreateTemp("", "image") if err != nil { - return fmt.Errorf("failed to create image dir: %v", err) + return fmt.Errorf("failed to create temporary image file: %v", err) } - - dstFile, err := os.Create(dstPath) - if err != nil { - return fmt.Errorf("failed to create image file: %v", err) - } - defer dstFile.Close() + defer os.Remove(tmpFile.Name()) // Parse image and process it. // If image is smaller than 600x400 or its ratio is less than 4:3, resize. @@ -211,7 +229,7 @@ func downloadBookImage(url, dstPath string) error { imgRatio := float64(imgWidth) / float64(imgHeight) if imgWidth >= 600 && imgHeight >= 400 && imgRatio > 1.3 { - err = jpeg.Encode(dstFile, img, nil) + err = jpeg.Encode(tmpFile, img, nil) } else { // Create background bg := image.NewNRGBA(imgRect) @@ -236,12 +254,44 @@ func downloadBookImage(url, dstPath string) error { draw.Draw(bg, bgRect, fg, fgPosition, draw.Over) // Save to file - err = jpeg.Encode(dstFile, bg, nil) + err = jpeg.Encode(tmpFile, bg, nil) } if err != nil { return fmt.Errorf("failed to save image %s: %v", url, err) } + err = MoveFileToDestination(dstPath, tmpFile) + if err != nil { + return err + } + + return nil +} + +// dstPath requires the filename +func MoveFileToDestination(dstPath string, tmpFile *os.File) error { + // Prepare destination file. + err := os.MkdirAll(fp.Dir(dstPath), model.DataDirPerm) + if err != nil { + return fmt.Errorf("failed to create destination dir: %v", err) + } + + dstFile, err := os.Create(dstPath) + if err != nil { + return fmt.Errorf("failed to create destination file: %v", err) + } + defer dstFile.Close() + // Copy temporary file to destination + _, err = tmpFile.Seek(0, io.SeekStart) + if err != nil { + return fmt.Errorf("failed to rewind temporary file: %v", err) + } + + _, err = io.Copy(dstFile, tmpFile) + if err != nil { + return fmt.Errorf("failed to copy file to the destination") + } + return nil } diff --git a/internal/core/processing_test.go b/internal/core/processing_test.go new file mode 100644 index 000000000..11e07af3f --- /dev/null +++ b/internal/core/processing_test.go @@ -0,0 +1,349 @@ +package core_test + +import ( + "bytes" + "net/http" + "net/http/httptest" + "os" + fp "path/filepath" + "testing" + + "github.com/go-shiori/shiori/internal/core" + "github.com/go-shiori/shiori/internal/model" + "github.com/stretchr/testify/assert" +) + +func TestMoveFileToDestination(t *testing.T) { + t.Run("create fails", func(t *testing.T) { + t.Run("directory create fails", func(t *testing.T) { + // test if create dir fails + tmpFile, err := os.CreateTemp("", "image") + + assert.NoError(t, err) + defer os.Remove(tmpFile.Name()) + + err = core.MoveFileToDestination("/destination/test", tmpFile) + assert.Error(t, err) + assert.Contains(t, err.Error(), "failed to create destination dir") + }) + t.Run("file create fails", func(t *testing.T) { + // if create file failed + tmpFile, err := os.CreateTemp("", "image") + assert.NoError(t, err) + defer os.Remove(tmpFile.Name()) + + // Create a destination directory + dstDir := t.TempDir() + assert.NoError(t, err) + defer os.Remove(dstDir) + + // Set destination path to an invalid file name to force os.Create to fail + dstPath := fp.Join(dstDir, "\000invalid\000") + + err = core.MoveFileToDestination(dstPath, tmpFile) + assert.Error(t, err) + assert.Contains(t, err.Error(), "failed to create destination file") + }) + }) +} +func TestDownloadBookImage(t *testing.T) { + t.Run("Download Images", func(t *testing.T) { + t.Run("fails", func(t *testing.T) { + // images is too small with unsupported format with a valid URL + imageURL := "https://github.com/go-shiori/shiori/blob/master/internal/view/assets/res/apple-touch-icon-152x152.png" + tempDir := t.TempDir() + dstPath := fp.Join(tempDir, "1") + defer os.Remove(dstPath) + + // Act + err := core.DownloadBookImage(imageURL, dstPath) + + // Assert + assert.EqualError(t, err, "unsupported image type") + assert.NoFileExists(t, dstPath) + }) + t.Run("sucssesful downlosd image", func(t *testing.T) { + // Arrange + imageURL := "https://raw.githubusercontent.com/go-shiori/shiori/master/docs/readme/cover.png" + tempDir := t.TempDir() + dstPath := fp.Join(tempDir, "1") + defer os.Remove(dstPath) + + // Act + err := core.DownloadBookImage(imageURL, dstPath) + + // Assert + assert.NoError(t, err) + assert.FileExists(t, dstPath) + }) + t.Run("sucssesful downlosd medium size image", func(t *testing.T) { + // create a file server handler for the 'testdata' directory + fs := http.FileServer(http.Dir("../../testdata/")) + + // start a test server with the file server handler + server := httptest.NewServer(fs) + defer server.Close() + + // Arrange + imageURL := server.URL + "/medium_image.png" + tempDir := t.TempDir() + dstPath := fp.Join(tempDir, "1") + defer os.Remove(dstPath) + + // Act + err := core.DownloadBookImage(imageURL, dstPath) + + // Assert + assert.NoError(t, err) + assert.FileExists(t, dstPath) + + }) + }) +} + +func TestProcessBookmark(t *testing.T) { + t.Run("ProcessRequest with sucssesful result", func(t *testing.T) { + t.Run("Normal without image", func(t *testing.T) { + bookmark := model.Bookmark{ + ID: 1, + URL: "https://example.com", + Title: "Example", + Excerpt: "This is an example article", + CreateEbook: true, + CreateArchive: true, + } + content := bytes.NewBufferString("

This is an example article

") + request := core.ProcessRequest{ + Bookmark: bookmark, + Content: content, + ContentType: "text/html", + DataDir: "/tmp", + KeepTitle: true, + KeepExcerpt: true, + } + expected, _, _ := core.ProcessBookmark(request) + + if expected.ID != bookmark.ID { + t.Errorf("Unexpected ID: got %v, want %v", expected.ID, bookmark.ID) + } + if expected.URL != bookmark.URL { + t.Errorf("Unexpected URL: got %v, want %v", expected.URL, bookmark.URL) + } + if expected.Title != bookmark.Title { + t.Errorf("Unexpected Title: got %v, want %v", expected.Title, bookmark.Title) + } + if expected.Excerpt != bookmark.Excerpt { + t.Errorf("Unexpected Excerpt: got %v, want %v", expected.Excerpt, bookmark.Excerpt) + } + }) + t.Run("Normal with multipleimage", func(t *testing.T) { + + html := `html + + + + + + +

This is an example article

+ + ` + bookmark := model.Bookmark{ + ID: 1, + URL: "https://example.com", + Title: "Example", + Excerpt: "This is an example article", + CreateEbook: true, + CreateArchive: true, + } + content := bytes.NewBufferString(html) + request := core.ProcessRequest{ + Bookmark: bookmark, + Content: content, + ContentType: "text/html", + DataDir: "/tmp", + KeepTitle: true, + KeepExcerpt: true, + } + expected, _, _ := core.ProcessBookmark(request) + + if expected.ID != bookmark.ID { + t.Errorf("Unexpected ID: got %v, want %v", expected.ID, bookmark.ID) + } + if expected.URL != bookmark.URL { + t.Errorf("Unexpected URL: got %v, want %v", expected.URL, bookmark.URL) + } + if expected.Title != bookmark.Title { + t.Errorf("Unexpected Title: got %v, want %v", expected.Title, bookmark.Title) + } + if expected.Excerpt != bookmark.Excerpt { + t.Errorf("Unexpected Excerpt: got %v, want %v", expected.Excerpt, bookmark.Excerpt) + } + }) + t.Run("ProcessRequest sucssesful with multipleimage included favicon and Thumbnail ", func(t *testing.T) { + // create a file server handler for the 'testdata' directory + fs := http.FileServer(http.Dir("../../testdata/")) + + // start a test server with the file server handler + server := httptest.NewServer(fs) + defer server.Close() + + html := `html + + + + + + +

This is an example article

+ + ` + bookmark := model.Bookmark{ + ID: 1, + URL: "https://example.com", + Title: "Example", + Excerpt: "This is an example article", + CreateEbook: true, + CreateArchive: true, + } + content := bytes.NewBufferString(html) + request := core.ProcessRequest{ + Bookmark: bookmark, + Content: content, + ContentType: "text/html", + DataDir: "/tmp", + KeepTitle: true, + KeepExcerpt: true, + } + expected, _, _ := core.ProcessBookmark(request) + + if expected.ID != bookmark.ID { + t.Errorf("Unexpected ID: got %v, want %v", expected.ID, bookmark.ID) + } + if expected.URL != bookmark.URL { + t.Errorf("Unexpected URL: got %v, want %v", expected.URL, bookmark.URL) + } + if expected.Title != bookmark.Title { + t.Errorf("Unexpected Title: got %v, want %v", expected.Title, bookmark.Title) + } + if expected.Excerpt != bookmark.Excerpt { + t.Errorf("Unexpected Excerpt: got %v, want %v", expected.Excerpt, bookmark.Excerpt) + } + }) + t.Run("ProcessRequest sucssesful with empty title ", func(t *testing.T) { + bookmark := model.Bookmark{ + ID: 1, + URL: "https://example.com", + Title: "", + Excerpt: "This is an example article", + CreateEbook: true, + CreateArchive: true, + } + content := bytes.NewBufferString("

This is an example article

") + request := core.ProcessRequest{ + Bookmark: bookmark, + Content: content, + ContentType: "text/html", + DataDir: "/tmp", + KeepTitle: true, + KeepExcerpt: true, + } + expected, _, _ := core.ProcessBookmark(request) + + if expected.ID != bookmark.ID { + t.Errorf("Unexpected ID: got %v, want %v", expected.ID, bookmark.ID) + } + if expected.URL != bookmark.URL { + t.Errorf("Unexpected URL: got %v, want %v", expected.URL, bookmark.URL) + } + if expected.Title != bookmark.URL { + t.Errorf("Unexpected Title: got %v, want %v", expected.Title, bookmark.Title) + } + if expected.Excerpt != bookmark.Excerpt { + t.Errorf("Unexpected Excerpt: got %v, want %v", expected.Excerpt, bookmark.Excerpt) + } + }) + t.Run("ProcessRequest sucssesful with empty Excerpt", func(t *testing.T) { + bookmark := model.Bookmark{ + ID: 1, + URL: "https://example.com", + Title: "", + Excerpt: "This is an example article", + CreateEbook: true, + CreateArchive: true, + } + content := bytes.NewBufferString("

This is an example article

") + request := core.ProcessRequest{ + Bookmark: bookmark, + Content: content, + ContentType: "text/html", + DataDir: "/tmp", + KeepTitle: true, + KeepExcerpt: false, + } + expected, _, _ := core.ProcessBookmark(request) + + if expected.ID != bookmark.ID { + t.Errorf("Unexpected ID: got %v, want %v", expected.ID, bookmark.ID) + } + if expected.URL != bookmark.URL { + t.Errorf("Unexpected URL: got %v, want %v", expected.URL, bookmark.URL) + } + if expected.Title != bookmark.URL { + t.Errorf("Unexpected Title: got %v, want %v", expected.Title, bookmark.Title) + } + if expected.Excerpt != bookmark.Excerpt { + t.Errorf("Unexpected Excerpt: got %v, want %v", expected.Excerpt, bookmark.Excerpt) + } + }) + t.Run("Specific case", func(t *testing.T) { + t.Run("ProcessRequest with ID zero", func(t *testing.T) { + + bookmark := model.Bookmark{ + ID: 0, + URL: "https://example.com", + Title: "Example", + Excerpt: "This is an example article", + CreateEbook: true, + CreateArchive: true, + } + content := bytes.NewBufferString("

This is an example article

") + request := core.ProcessRequest{ + Bookmark: bookmark, + Content: content, + ContentType: "text/html", + DataDir: "/tmp", + KeepTitle: true, + KeepExcerpt: true, + } + _, isFatal, err := core.ProcessBookmark(request) + assert.Error(t, err) + assert.Contains(t, err.Error(), "bookmark ID is not valid") + assert.True(t, isFatal) + }) + + t.Run("ProcessRequest that content type not zero", func(t *testing.T) { + + bookmark := model.Bookmark{ + ID: 1, + URL: "https://example.com", + Title: "Example", + Excerpt: "This is an example article", + CreateEbook: true, + CreateArchive: true, + } + content := bytes.NewBufferString("

This is an example article

") + request := core.ProcessRequest{ + Bookmark: bookmark, + Content: content, + ContentType: "application/pdf", + DataDir: "/tmp", + KeepTitle: true, + KeepExcerpt: true, + } + _, _, err := core.ProcessBookmark(request) + assert.NoError(t, err) + }) + }) + }) +} diff --git a/internal/view/assets/js/page/home.js b/internal/view/assets/js/page/home.js index 1ff6feddd..70b589af0 100644 --- a/internal/view/assets/js/page/home.js +++ b/internal/view/assets/js/page/home.js @@ -702,15 +702,36 @@ export default { this.editMode = false; this.dialog.loading = false; this.dialog.visible = false; - - json.forEach(book => { - var item = items.find(el => el.id === book.id); - this.bookmarks.splice(item.index, 1, book); - }); - }).catch(err => { - this.selection = []; - this.editMode = false; - this.dialog.loading = false; + + let faildedUpdateArchives = []; + let faildedCreateEbook = []; + json.forEach(book => { + var item = items.find(el => el.id === book.id); + this.bookmarks.splice(item.index, 1, book); + + if (data.createArchive && !book.hasArchive){ + faildedUpdateArchives.push(book.id); + console.error("can't update archive for bookmark id", book.id) + } + if (data.createEbook && !book.hasEbook){ + faildedCreateEbook.push(book.id); + console.error("can't update ebook for bookmark id:", book.id) + } + }); + if(faildedCreateEbook.length > 0 || faildedUpdateArchives.length > 0){ + this.showDialog({ + title: `Bookmarks Id that Update Action Faild`, + content: `Not all bookmarks could have their contents updated, but no files were overwritten.`, + mainText: "OK", + mainClick: () => { + this.dialog.visible = false; + }, + }) + } + }).catch(err => { + this.selection = []; + this.editMode = false; + this.dialog.loading = false; this.getErrorMessage(err).then(msg => { this.showErrorDialog(msg); diff --git a/internal/webserver/handler-api.go b/internal/webserver/handler-api.go index 790324f5b..1e2271794 100644 --- a/internal/webserver/handler-api.go +++ b/internal/webserver/handler-api.go @@ -434,12 +434,32 @@ func (h *Handler) ApiDownloadEbook(w http.ResponseWriter, r *http.Request, ps ht ContentType: contentType, } - book, err = core.GenerateEbook(request) - content.Close() + // if file exist book return avilable file + strID := strconv.Itoa(book.ID) + ebookPath := fp.Join(request.DataDir, "ebook", strID+".epub") + _, err = os.Stat(ebookPath) + if err == nil { + // file already exists, return the existing file + imagePath := fp.Join(request.DataDir, "thumb", fmt.Sprintf("%d", book.ID)) + archivePath := fp.Join(request.DataDir, "archive", fmt.Sprintf("%d", book.ID)) + + if _, err := os.Stat(imagePath); err == nil { + book.ImageURL = fp.Join("/", "bookmark", strID, "thumb") + } - if err != nil { - chProblem <- book.ID - return + if _, err := os.Stat(archivePath); err == nil { + book.HasArchive = true + } + book.HasEbook = true + } else { + // generate ebook file + book, err = core.GenerateEbook(request, ebookPath) + content.Close() + + if err != nil { + chProblem <- book.ID + return + } } // Update list of bookmarks diff --git a/internal/webserver/handler-ui.go b/internal/webserver/handler-ui.go index 2f300aea4..712744f51 100644 --- a/internal/webserver/handler-ui.go +++ b/internal/webserver/handler-ui.go @@ -48,7 +48,7 @@ func (h *Handler) ServeBookmarkContent(w http.ResponseWriter, r *http.Request, p } } - // Check if it has archive. + // Check if it has ebook. ebookPath := fp.Join(h.DataDir, "ebook", strID+".epub") if fileExists(ebookPath) { bookmark.HasEbook = true diff --git a/testdata/big_image.png b/testdata/big_image.png new file mode 100644 index 000000000..e1a237262 Binary files /dev/null and b/testdata/big_image.png differ diff --git a/testdata/favicon.png b/testdata/favicon.png new file mode 100644 index 000000000..eb6c03775 Binary files /dev/null and b/testdata/favicon.png differ diff --git a/testdata/favicon.svg b/testdata/favicon.svg new file mode 100644 index 000000000..bd36e52d7 --- /dev/null +++ b/testdata/favicon.svg @@ -0,0 +1,6 @@ + + + + + diff --git a/testdata/medium_image.png b/testdata/medium_image.png new file mode 100644 index 000000000..bce2a0cce Binary files /dev/null and b/testdata/medium_image.png differ