reddit: fix image copy for existing should go to temp dir first

This commit is contained in:
Tigor Hutasuhut 2024-05-01 00:02:30 +07:00
parent c5c8058376
commit 65bb5732ca
5 changed files with 66 additions and 31 deletions

View file

@ -5,10 +5,12 @@ import (
"errors" "errors"
"image/jpeg" "image/jpeg"
"io" "io"
"io/fs"
"net/http" "net/http"
"net/url" "net/url"
"os" "os"
"path" "path"
"path/filepath"
"strings" "strings"
"sync" "sync"
"time" "time"
@ -185,21 +187,32 @@ func (api *API) downloadSubredditImage(ctx context.Context, post reddit.Post, su
return api.saveImageToFSAndDatabase(ctx, tmpImageFile, subreddit, post, devices) return api.saveImageToFSAndDatabase(ctx, tmpImageFile, subreddit, post, devices)
} }
type stat interface {
Stat() (fs.FileInfo, error)
}
func (api *API) saveImageToFSAndDatabase(ctx context.Context, image io.ReadCloser, subreddit *models.Subreddit, post reddit.Post, devices models.DeviceSlice) (err error) { func (api *API) saveImageToFSAndDatabase(ctx context.Context, image io.ReadCloser, subreddit *models.Subreddit, post reddit.Post, devices models.DeviceSlice) (err error) {
ctx, span := tracer.Start(ctx, "*API.saveImageToFSAndDatabase") ctx, span := tracer.Start(ctx, "*API.saveImageToFSAndDatabase")
defer span.End() defer span.End()
defer image.Close() defer image.Close()
w, close, err := api.createDeviceImageWriters(post, devices) w, close, err := api.createDeviceImageWriters(post, devices)
defer close()
if err != nil { if err != nil {
return errs.Wrapw(err, "failed to create image files") return errs.Wrapw(err, "failed to create image files")
} }
log.New(ctx).Debug("saving image files", "post_id", post.GetID(), "post_url", post.GetImageURL(), "devices", devices) log.New(ctx).Debug("saving image files", "post_id", post.GetID(), "post_url", post.GetImageURL(), "devices", devices)
defer close() size, err := io.Copy(w, image)
_, err = io.Copy(w, image)
if err != nil { if err != nil {
return errs.Wrapw(err, "failed to save image files") return errs.Wrapw(err, "failed to save image files")
} }
if size == 0 {
if s, ok := image.(stat); ok {
if fi, err := s.Stat(); err == nil {
size = fi.Size()
}
}
}
var many []*models.ImageSetter var many []*models.ImageSetter
now := time.Now() now := time.Now()
@ -209,10 +222,6 @@ func (api *API) saveImageToFSAndDatabase(ctx context.Context, image io.ReadClose
nsfw = 1 nsfw = 1
} }
width, height := post.GetImageSize() width, height := post.GetImageSize()
var size int64
if fi, err := os.Stat(post.GetImageTargetPath(api.config, device)); err == nil {
size = fi.Size()
}
many = append(many, &models.ImageSetter{ many = append(many, &models.ImageSetter{
Subreddit: omit.From(subreddit.Name), Subreddit: omit.From(subreddit.Name),
@ -300,14 +309,14 @@ func (api *API) isImageEntryExists(ctx context.Context, post reddit.Post, device
ctx, span := tracer.Start(ctx, "*API.IsImageExists") ctx, span := tracer.Start(ctx, "*API.IsImageExists")
defer span.End() defer span.End()
_, errQuery := models.Images.Query(ctx, api.db, exist, errQuery := models.Images.Query(ctx, api.db,
models.SelectWhere.Images.Device.EQ(device.Slug), models.SelectWhere.Images.Device.EQ(device.Slug),
models.SelectWhere.Images.PostName.EQ(post.GetName()), models.SelectWhere.Images.PostName.EQ(post.GetName()),
).One() ).Exists()
_, errStat := os.Stat(post.GetImageTargetPath(api.config, device)) _, errStat := os.Stat(post.GetImageTargetPath(api.config, device))
return errQuery == nil && errStat == nil return exist && errQuery == nil && errStat == nil
} }
// findImageFileForDevice finds if any of the image file exists for given devices. // findImageFileForDevice finds if any of the image file exists for given devices.
@ -317,16 +326,39 @@ func (api *API) isImageEntryExists(ctx context.Context, post reddit.Post, device
// Return nil if no image file exists for the devices. // Return nil if no image file exists for the devices.
// //
// Ensure to close the file after use. // Ensure to close the file after use.
func (api *API) findImageFileForDevices(ctx context.Context, post reddit.Post, devices models.DeviceSlice) (file *os.File) { func (api *API) findImageFileForDevices(ctx context.Context, post reddit.Post, devices models.DeviceSlice) (oldImageFile *os.File) {
for _, device := range devices { for _, device := range devices {
_, err := os.Stat(post.GetImageTargetPath(api.config, device)) stat, err := os.Stat(post.GetImageTargetPath(api.config, device))
if err == nil { if err == nil {
file, err = os.Open(post.GetImageTargetPath(api.config, device)) oldImageFile, err = os.Open(post.GetImageTargetPath(api.config, device))
if err != nil { if err != nil {
log.New(ctx).Err(err).Error("failed to open image file", "filename", post.GetImageTargetPath(api.config, device)) log.New(ctx).Err(err).Error("failed to open image file", "filename", post.GetImageTargetPath(api.config, device))
return nil return nil
} }
return file defer oldImageFile.Close()
tempFilename := filepath.Join(os.TempDir(), "redmage", stat.Name())
tempFileWrite, err := os.Create(tempFilename)
if err != nil {
log.New(ctx).Err(err).Error("failed to create temp file", "filename", post.GetImageTargetPath(api.config, device))
return nil
}
defer tempFileWrite.Close()
_, err = io.Copy(tempFileWrite, oldImageFile)
if err != nil {
log.New(ctx).Err(err).Error("failed to copy image file", "filename", post.GetImageTargetPath(api.config, device))
return nil
}
rf, err := os.Open(tempFilename)
if err != nil {
log.New(ctx).Err(err).Error("failed to open temp file", "filename", tempFileWrite.Name())
return nil
}
return rf
} }
} }

View file

@ -2,7 +2,7 @@ POST http://localhost:8080/api/v1/subreddits HTTP/1.1
Host: localhost:8080 Host: localhost:8080
{ {
"name": "wallpapers", "name": "wallpaper",
"enable_schedule": 1, "enable_schedule": 1,
"schedule": "@daily", "schedule": "@daily",
"countback": 300 "countback": 300

View file

@ -3,5 +3,5 @@ Host: localhost:8080
Content-Type: application/json Content-Type: application/json
{ {
"subreddit": "wallpapers" "subreddit": "wallpaper"
} }

View file

@ -39,8 +39,7 @@ templ HomeContent(c *views.Context, data Data) {
hx-push-url="true" hx-push-url="true"
> >
<h1 class="mb-4"> <h1 class="mb-4">
Recently Added - Recently Added
{ strconv.FormatInt(data.TotalImages, 10) } Images
</h1> </h1>
@recentRangeInput(c) @recentRangeInput(c)
@nsfwToggle(c, data) @nsfwToggle(c, data)
@ -48,6 +47,10 @@ templ HomeContent(c *views.Context, data Data) {
<div id="recently-added-images"> <div id="recently-added-images">
if data.TotalImages == 0 { if data.TotalImages == 0 {
<h2 class="mt-4">There are no recently added images in the current time range.</h2> <h2 class="mt-4">There are no recently added images in the current time range.</h2>
} else {
<h2 class="mt-4">
Added Images: { strconv.FormatInt(data.TotalImages, 10) }
</h2>
} }
for _, recently := range data.RecentlyAddedImages { for _, recently := range data.RecentlyAddedImages {
<div class="divider"></div> <div class="divider"></div>

View file

@ -1,9 +1,10 @@
package homeview package homeview
import "github.com/tigorlazuardi/redmage/models" import "github.com/tigorlazuardi/redmage/models"
import "github.com/tigorlazuardi/redmage/api/reddit"
import "fmt" import "fmt"
import "github.com/tigorlazuardi/redmage/views/utils" import "github.com/tigorlazuardi/redmage/views/utils"
import "strconv"
import "github.com/alecthomas/units"
type ImageCardOption uint type ImageCardOption uint
@ -19,14 +20,14 @@ const (
) )
templ RecentlyAddedImageCard(data *models.Image, opts ImageCardOption) { templ RecentlyAddedImageCard(data *models.Image, opts ImageCardOption) {
<div class="not-prose card card-bordered bg-base-100 hover:bg-base-200 shadow-xl w-[256px] min-w-[256px] rounded-xl top-0 hover:-top-1 hover:drop-shadow-2xl transition-all"> <div class="not-prose card card-bordered bg-base-100 hover:bg-base-200 shadow-xl min-w-[256px] rounded-xl top-0 hover:-top-1 hover:drop-shadow-2xl transition-all">
<figure> <figure>
<a <a
href={ templ.URL(fmt.Sprintf("/img/%s", data.ImageRelativePath)) } href={ templ.URL(fmt.Sprintf("/img/%s", data.ImageRelativePath)) }
target="_blank" target="_blank"
> >
<img <img
class="object-contain w-[256px] h-[256px]" class="object-contain max-w-[256px] max-h-[256px]"
src={ fmt.Sprintf("/img/%s", data.ThumbnailRelativePath) } src={ fmt.Sprintf("/img/%s", data.ThumbnailRelativePath) }
alt={ data.PostTitle } alt={ data.PostTitle }
/> />
@ -40,18 +41,17 @@ templ RecentlyAddedImageCard(data *models.Image, opts ImageCardOption) {
>{ truncateTitle(data.PostTitle) }</a> >{ truncateTitle(data.PostTitle) }</a>
} }
<a class="text-primary underline" href={ templ.URL(data.PostAuthorURL) }>{ data.PostAuthor }</a> <a class="text-primary underline" href={ templ.URL(data.PostAuthorURL) }>{ data.PostAuthor }</a>
<div> <div class="flex-1"></div>
@utils.RelativeTimeNode(fmt.Sprintf("relative-time-%s", data.PostName), data.CreatedAt) <div class="flex w-full justify-end">
@utils.RelativeTimeNode(fmt.Sprintf("relative-time-%s", data.PostName), data.CreatedAt, "text-sm")
</div> </div>
<div class="card-actions justify-between"> <div class="grid grid-cols-2 gap-x-4">
if data.R.Subreddit != nil { <p class="text-xs">Width</p>
if !opts.Has(HideSubreddit) { <p class="text-xs text-end">{ strconv.Itoa(int(data.ImageWidth)) } px</p>
<a <p class="text-xs">Height</p>
class="badge badge-outline badge-primary" <p class="text-xs text-end">{ strconv.Itoa(int(data.ImageHeight)) } px</p>
href={ templ.URL(fmt.Sprintf("https://reddit.com/%s/%s", reddit.SubredditType(data.R.Subreddit.Subtype), data.R.Subreddit.Name)) } <p class="text-xs">Size</p>
>{ data.R.Subreddit.Name } </a> <p class="text-xs text-end">{ units.MetricBytes(data.ImageSize).Round(1).String() }</p>
}
}
</div> </div>
</div> </div>
</div> </div>