mirror of
https://github.com/JuLi0n21/thumbnailservice.git
synced 2026-04-20 00:10:07 +00:00
avoid collisions and delete the thumnbnail after successful response
This commit is contained in:
@@ -8,6 +8,7 @@ import (
|
|||||||
"path"
|
"path"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
pb "thumbnailclient/proto"
|
pb "thumbnailclient/proto"
|
||||||
@@ -32,13 +33,20 @@ func main() {
|
|||||||
{pb.FileType_PDF, "testdata/pdf-sample.pdf"},
|
{pb.FileType_PDF, "testdata/pdf-sample.pdf"},
|
||||||
{pb.FileType_VIDEO, "testdata/video-sample.webm"}}
|
{pb.FileType_VIDEO, "testdata/video-sample.webm"}}
|
||||||
|
|
||||||
|
a := sync.WaitGroup{}
|
||||||
|
|
||||||
for _, f := range filePath {
|
for _, f := range filePath {
|
||||||
newFunction(f.Path, f.Type, client)
|
a.Add(1)
|
||||||
|
go func() {
|
||||||
|
createPreview(f.Path, f.Type, client)
|
||||||
|
a.Done()
|
||||||
|
}()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
a.Wait()
|
||||||
}
|
}
|
||||||
|
|
||||||
func newFunction(filePath string, ftype pb.FileType, client pb.ThumbnailServiceClient) {
|
func createPreview(filePath string, ftype pb.FileType, client pb.ThumbnailServiceClient) {
|
||||||
fileContent, err := os.ReadFile(filePath)
|
fileContent, err := os.ReadFile(filePath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("Error reading file: %v", err)
|
log.Fatalf("Error reading file: %v", err)
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ module github.com/JuLi0n21/thumbnail_service
|
|||||||
go 1.24.1
|
go 1.24.1
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
github.com/google/uuid v1.6.0
|
||||||
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646
|
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646
|
||||||
google.golang.org/grpc v1.71.0
|
google.golang.org/grpc v1.71.0
|
||||||
google.golang.org/protobuf v1.36.6
|
google.golang.org/protobuf v1.36.6
|
||||||
|
|||||||
@@ -22,25 +22,15 @@ go.opentelemetry.io/otel/sdk/metric v1.34.0 h1:5CeK9ujjbFVL5c1PhLuStg1wxA7vQv7ce
|
|||||||
go.opentelemetry.io/otel/sdk/metric v1.34.0/go.mod h1:jQ/r8Ze28zRKoNRdkjCZxfs6YvBTG1+YIqyFVFYec5w=
|
go.opentelemetry.io/otel/sdk/metric v1.34.0/go.mod h1:jQ/r8Ze28zRKoNRdkjCZxfs6YvBTG1+YIqyFVFYec5w=
|
||||||
go.opentelemetry.io/otel/trace v1.34.0 h1:+ouXS2V8Rd4hp4580a8q23bg0azF2nI8cqLYnC8mh/k=
|
go.opentelemetry.io/otel/trace v1.34.0 h1:+ouXS2V8Rd4hp4580a8q23bg0azF2nI8cqLYnC8mh/k=
|
||||||
go.opentelemetry.io/otel/trace v1.34.0/go.mod h1:Svm7lSjQD7kG7KJ/MUHPVXSDGz2OX4h0M2jHBhmSfRE=
|
go.opentelemetry.io/otel/trace v1.34.0/go.mod h1:Svm7lSjQD7kG7KJ/MUHPVXSDGz2OX4h0M2jHBhmSfRE=
|
||||||
golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0=
|
|
||||||
golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k=
|
|
||||||
golang.org/x/net v0.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c=
|
golang.org/x/net v0.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c=
|
||||||
golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
|
golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
|
||||||
golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
|
|
||||||
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
|
||||||
golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
|
golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
|
||||||
golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||||
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
|
|
||||||
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
|
||||||
golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
|
golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
|
||||||
golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
|
golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f h1:OxYkA3wjPsZyBylwymxSHa7ViiW1Sml4ToBrncvFehI=
|
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f/go.mod h1:+2Yz8+CLJbIfL9z73EW45avw8Lmge3xVElCP9zEKi50=
|
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250313205543-e70fdf4c4cb4 h1:iK2jbkWL86DXjEx0qiHcRE9dE4/Ahua5k6V8OWFb//c=
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20250313205543-e70fdf4c4cb4 h1:iK2jbkWL86DXjEx0qiHcRE9dE4/Ahua5k6V8OWFb//c=
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250313205543-e70fdf4c4cb4/go.mod h1:LuRYeWDFV6WOn90g357N17oMCaxpgCnbi/44qJvDn2I=
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20250313205543-e70fdf4c4cb4/go.mod h1:LuRYeWDFV6WOn90g357N17oMCaxpgCnbi/44qJvDn2I=
|
||||||
google.golang.org/grpc v1.71.0 h1:kF77BGdPTQ4/JZWMlb9VpJ5pa25aqvVqogsxNHHdeBg=
|
google.golang.org/grpc v1.71.0 h1:kF77BGdPTQ4/JZWMlb9VpJ5pa25aqvVqogsxNHHdeBg=
|
||||||
google.golang.org/grpc v1.71.0/go.mod h1:H0GRtasmQOh9LkFoCPDu3ZrwUtD1YGE+b2vYBYd/8Ec=
|
google.golang.org/grpc v1.71.0/go.mod h1:H0GRtasmQOh9LkFoCPDu3ZrwUtD1YGE+b2vYBYd/8Ec=
|
||||||
google.golang.org/protobuf v1.36.4 h1:6A3ZDJHn/eNqc1i+IdefRzy/9PokBTPvcqMySR7NNIM=
|
|
||||||
google.golang.org/protobuf v1.36.4/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
|
|
||||||
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
|
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
|
||||||
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
|
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
|
||||||
|
|||||||
@@ -8,22 +8,22 @@ import (
|
|||||||
"net"
|
"net"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
_ "image/gif"
|
_ "image/gif"
|
||||||
"image/jpeg"
|
"image/jpeg"
|
||||||
_ "image/jpeg"
|
|
||||||
"image/png"
|
"image/png"
|
||||||
_ "image/png"
|
|
||||||
|
|
||||||
pb "github.com/JuLi0n21/thumbnail_service/proto"
|
pb "github.com/JuLi0n21/thumbnail_service/proto"
|
||||||
|
"github.com/google/uuid"
|
||||||
"github.com/nfnt/resize"
|
"github.com/nfnt/resize"
|
||||||
"google.golang.org/grpc"
|
"google.golang.org/grpc"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Helper function to generate video thumbnails using ffmpeg
|
|
||||||
func generateVideoThumbnail(inputPath, outputPath string, maxWidth, maxHeight int) error {
|
func generateVideoThumbnail(inputPath, outputPath string, maxWidth, maxHeight int) error {
|
||||||
cmd := exec.Command("ffmpeg", "-i", inputPath, "-vf", "thumbnail", "-frames:v", "1", outputPath)
|
cmd := exec.Command("ffmpeg", "-y", "-i", inputPath, "-vf", "thumbnail", "-frames:v", "1", outputPath)
|
||||||
var stderr strings.Builder
|
var stderr strings.Builder
|
||||||
cmd.Stderr = &stderr
|
cmd.Stderr = &stderr
|
||||||
|
|
||||||
@@ -39,24 +39,21 @@ func generateVideoThumbnail(inputPath, outputPath string, maxWidth, maxHeight in
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper function to generate PDF thumbnails using poppler-utils (pdftoppm)
|
|
||||||
func generatePdfThumbnail(inputPath, outputPath string, maxWidth, maxHeight int) error {
|
func generatePdfThumbnail(inputPath, outputPath string, maxWidth, maxHeight int) error {
|
||||||
// Command for Poppler-utils to generate a thumbnail from the first page of a PDF file
|
|
||||||
cmd := exec.Command("pdftoppm", inputPath, outputPath, "-jpeg", "-f", "1", "-l", "1", "-scale-to", "200")
|
cmd := exec.Command("pdftoppm", inputPath, outputPath, "-jpeg", "-f", "1", "-l", "1", "-scale-to", "200")
|
||||||
err := cmd.Run()
|
err := cmd.Run()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to generate PDF thumbnail using Poppler-utils: %v", err)
|
return fmt.Errorf("failed to generate PDF thumbnail using Poppler-utils: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
outputFileWithPage := fmt.Sprintf("%s-01.jpg", outputPath) // pdftoppm output file with page number suffix
|
outputFileWithPage := fmt.Sprintf("%s-01.jpg", outputPath)
|
||||||
|
|
||||||
// Rename the file
|
|
||||||
err = os.Rename(outputFileWithPage, outputPath)
|
err = os.Rename(outputFileWithPage, outputPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to rename file: %v", err)
|
return fmt.Errorf("failed to rename file: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Resize the generated image if maxWidth or maxHeight is provided
|
|
||||||
if maxWidth > 0 || maxHeight > 0 {
|
if maxWidth > 0 || maxHeight > 0 {
|
||||||
return resizeImage(outputPath, outputPath, maxWidth, maxHeight)
|
return resizeImage(outputPath, outputPath, maxWidth, maxHeight)
|
||||||
}
|
}
|
||||||
@@ -65,43 +62,39 @@ func generatePdfThumbnail(inputPath, outputPath string, maxWidth, maxHeight int)
|
|||||||
}
|
}
|
||||||
|
|
||||||
func resizeImage(inputPath, outputPath string, maxWidth, maxHeight int) error {
|
func resizeImage(inputPath, outputPath string, maxWidth, maxHeight int) error {
|
||||||
// Open the input image file
|
|
||||||
file, err := os.Open(inputPath)
|
file, err := os.Open(inputPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to open image file: %v", err)
|
return fmt.Errorf("failed to open image file: %v", err)
|
||||||
}
|
}
|
||||||
defer file.Close()
|
defer file.Close()
|
||||||
|
|
||||||
// Decode the image
|
|
||||||
img, imgType, err := image.Decode(file)
|
img, imgType, err := image.Decode(file)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to decode image: %v", err)
|
return fmt.Errorf("failed to decode image: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Calculate the new dimensions, preserving the aspect ratio
|
|
||||||
var newWidth, newHeight int
|
var newWidth, newHeight int
|
||||||
if maxWidth > 0 && maxHeight > 0 {
|
if maxWidth > 0 && maxHeight > 0 {
|
||||||
// Resize with both width and height limit
|
|
||||||
newWidth = maxWidth
|
newWidth = maxWidth
|
||||||
newHeight = maxHeight
|
newHeight = maxHeight
|
||||||
} else if maxWidth > 0 {
|
} else if maxWidth > 0 {
|
||||||
// Resize based on width
|
|
||||||
newWidth = maxWidth
|
newWidth = maxWidth
|
||||||
newHeight = int(float64(img.Bounds().Dy()) * float64(maxWidth) / float64(img.Bounds().Dx()))
|
newHeight = int(float64(img.Bounds().Dy()) * float64(maxWidth) / float64(img.Bounds().Dx()))
|
||||||
} else if maxHeight > 0 {
|
} else if maxHeight > 0 {
|
||||||
// Resize based on height
|
|
||||||
newHeight = maxHeight
|
newHeight = maxHeight
|
||||||
newWidth = int(float64(img.Bounds().Dx()) * float64(maxHeight) / float64(img.Bounds().Dy()))
|
newWidth = int(float64(img.Bounds().Dx()) * float64(maxHeight) / float64(img.Bounds().Dy()))
|
||||||
} else {
|
} else {
|
||||||
// No resizing needed
|
|
||||||
newWidth = img.Bounds().Dx()
|
newWidth = img.Bounds().Dx()
|
||||||
newHeight = img.Bounds().Dy()
|
newHeight = img.Bounds().Dy()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Resize the image using the calculated dimensions
|
|
||||||
resizedImg := resize.Resize(uint(newWidth), uint(newHeight), img, resize.Lanczos3)
|
resizedImg := resize.Resize(uint(newWidth), uint(newHeight), img, resize.Lanczos3)
|
||||||
|
|
||||||
// Create the output file
|
|
||||||
outFile, err := os.Create(outputPath)
|
outFile, err := os.Create(outputPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to create output file: %v", err)
|
return fmt.Errorf("failed to create output file: %v", err)
|
||||||
@@ -130,79 +123,72 @@ type server struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *server) GenerateThumbnail(ctx context.Context, req *pb.ThumbnailRequest) (*pb.ThumbnailResponse, error) {
|
func (s *server) GenerateThumbnail(ctx context.Context, req *pb.ThumbnailRequest) (*pb.ThumbnailResponse, error) {
|
||||||
// Create a temporary file to store the uploaded content
|
start := time.Now()
|
||||||
|
fmt.Println(start.Format("2006-01-02 15:04:05.000"), "Thumbnail request ", req.FileType, "H: ", req.MaxHeight, "W: ", req.MaxWidth)
|
||||||
|
|
||||||
tempFile, err := os.CreateTemp("", "upload-*")
|
tempFile, err := os.CreateTemp("", "upload-*")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to create temporary file: %v", err)
|
return nil, fmt.Errorf("failed to create temporary file: %v", err)
|
||||||
}
|
}
|
||||||
defer os.Remove(tempFile.Name()) // Cleanup temporary file
|
defer os.Remove(tempFile.Name())
|
||||||
|
|
||||||
// Write the content from the request to the temporary file
|
|
||||||
err = os.WriteFile(tempFile.Name(), req.FileContent, 0644)
|
err = os.WriteFile(tempFile.Name(), req.FileContent, 0644)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to write content to file: %v", err)
|
return nil, fmt.Errorf("failed to write content to file: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate thumbnail and save it to disk based on file type
|
thumbnailName := fmt.Sprintf("thumbnail-%s.jpg", uuid.New().String())
|
||||||
var outputPath string
|
outputPath := filepath.Join("thumbnails", thumbnailName)
|
||||||
|
|
||||||
|
if err := os.MkdirAll("thumbnails", 0755); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to create thumbnails directory: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
// Check file type using enum
|
|
||||||
switch req.FileType {
|
switch req.FileType {
|
||||||
case pb.FileType_IMAGE:
|
case pb.FileType_IMAGE:
|
||||||
// Image file, use ImageMagick or other Go logic to create a thumbnail
|
|
||||||
outputPath = "thumbnails/image-thumbnail.jpg"
|
|
||||||
err = resizeImage(tempFile.Name(), outputPath, int(req.MaxWidth), int(req.MaxHeight))
|
err = resizeImage(tempFile.Name(), outputPath, int(req.MaxWidth), int(req.MaxHeight))
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
case pb.FileType_VIDEO:
|
case pb.FileType_VIDEO:
|
||||||
// Video file, use FFmpeg to create a thumbnail
|
|
||||||
outputPath = "thumbnails/video-thumbnail.jpg" // Video thumbnails are typically saved as JPG
|
|
||||||
err = generateVideoThumbnail(tempFile.Name(), outputPath, int(req.MaxWidth), int(req.MaxHeight))
|
err = generateVideoThumbnail(tempFile.Name(), outputPath, int(req.MaxWidth), int(req.MaxHeight))
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
case pb.FileType_PDF:
|
case pb.FileType_PDF:
|
||||||
// PDF file, use Poppler-utils to create a thumbnail
|
|
||||||
outputPath = "thumbnails/pdf-thumbnail.jpg" // PDF thumbnails are typically saved as JPG
|
|
||||||
err = generatePdfThumbnail(tempFile.Name(), outputPath, int(req.MaxWidth), int(req.MaxHeight))
|
err = generatePdfThumbnail(tempFile.Name(), outputPath, int(req.MaxWidth), int(req.MaxHeight))
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return nil, fmt.Errorf("unsupported file type: %v", req.FileType)
|
return nil, fmt.Errorf("unsupported file type: %v", req.FileType)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Read the generated thumbnail back into memory to send it as bytes
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
if _, err := os.Stat(outputPath); err == nil {
|
||||||
|
os.Remove(outputPath)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
thumbnailContent, err := os.ReadFile(outputPath)
|
thumbnailContent, err := os.ReadFile(outputPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to read generated thumbnail: %v", err)
|
return nil, fmt.Errorf("failed to read generated thumbnail: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Return the response with the thumbnail bytes and output path
|
end := time.Since(time.Now())
|
||||||
|
fmt.Println(time.Now().Format("2006-01-02 15:04:05.000"), "Finshed in: ", end, req.FileType, "H: ", req.MaxHeight, "W: ", req.MaxWidth)
|
||||||
return &pb.ThumbnailResponse{
|
return &pb.ThumbnailResponse{
|
||||||
Message: "Thumbnail generated successfully",
|
Message: "Thumbnail generated successfully",
|
||||||
ThumbnailContent: thumbnailContent, // Send the thumbnail as bytes
|
ThumbnailContent: thumbnailContent,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
// Set up a listener on port 50051
|
|
||||||
listen, err := net.Listen("tcp", ":50051")
|
listen, err := net.Listen("tcp", ":50051")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("Failed to listen: %v", err)
|
log.Fatalf("Failed to listen: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create a gRPC server
|
|
||||||
grpcServer := grpc.NewServer()
|
grpcServer := grpc.NewServer()
|
||||||
|
|
||||||
// Register the server
|
|
||||||
pb.RegisterThumbnailServiceServer(grpcServer, &server{})
|
pb.RegisterThumbnailServiceServer(grpcServer, &server{})
|
||||||
|
|
||||||
// Start serving requests
|
|
||||||
log.Println("Server started on port 50051")
|
log.Println("Server started on port 50051")
|
||||||
if err := grpcServer.Serve(listen); err != nil {
|
if err := grpcServer.Serve(listen); err != nil {
|
||||||
log.Fatalf("Failed to serve: %v", err)
|
log.Fatalf("Failed to serve: %v", err)
|
||||||
|
|||||||
0
server/thumbnails/.gitkeep
Normal file
0
server/thumbnails/.gitkeep
Normal file
Binary file not shown.
|
Before Width: | Height: | Size: 166 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 3.7 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 6.1 KiB |
Reference in New Issue
Block a user