Init
This commit is contained in:
144
internal/file/handlers.go
Normal file
144
internal/file/handlers.go
Normal file
@@ -0,0 +1,144 @@
|
||||
package file
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type Handler struct {
|
||||
service *Service
|
||||
}
|
||||
|
||||
func NewHandler(s *Service) *Handler {
|
||||
return &Handler{service: s}
|
||||
}
|
||||
|
||||
func (h *Handler) Upload(c *gin.Context) {
|
||||
err := c.Request.ParseMultipartForm(0)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
file, err := c.FormFile("file")
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "missing file"})
|
||||
return
|
||||
}
|
||||
|
||||
f, err := file.Open()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "cannot open file"})
|
||||
return
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
once := c.PostForm("once") == "true"
|
||||
|
||||
durationStr := c.PostForm("duration")
|
||||
hours, err := strconv.Atoi(durationStr)
|
||||
if err != nil || hours <= 0 {
|
||||
hours = 24 // default
|
||||
}
|
||||
|
||||
duration := time.Duration(hours) * time.Hour
|
||||
|
||||
record, err := h.service.UploadFile(
|
||||
file.Filename,
|
||||
f,
|
||||
once,
|
||||
duration,
|
||||
)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"id": record.ID,
|
||||
"deletion_id": record.DeletionID,
|
||||
"filename": record.Filename,
|
||||
"size": record.Size,
|
||||
"expires_at": record.ExpiresAt,
|
||||
})
|
||||
}
|
||||
|
||||
func (h *Handler) Download(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
|
||||
record, err := h.service.DownloadFile(id)
|
||||
if err != nil {
|
||||
c.HTML(http.StatusOK, "fileNotFound.html", nil)
|
||||
return
|
||||
}
|
||||
c.Header("Content-Disposition", fmt.Sprintf(`inline; filename="%s"`, record.Filename))
|
||||
c.File(record.Path)
|
||||
}
|
||||
|
||||
func (h *Handler) Delete(c *gin.Context) {
|
||||
id := c.Param("del_id")
|
||||
|
||||
_, err := h.service.DeleteFileByDeletionID(id)
|
||||
if err != nil {
|
||||
c.HTML(http.StatusOK, "fileNotFound.html", nil)
|
||||
return
|
||||
}
|
||||
|
||||
//c.JSON(http.StatusOK, gin.H{"status": "deleted"})
|
||||
c.HTML(http.StatusOK, "deleted.html", nil)
|
||||
}
|
||||
|
||||
func (h *Handler) AdminList(c *gin.Context) {
|
||||
records, err := h.service.repo.GetAll()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, records)
|
||||
}
|
||||
|
||||
func (h *Handler) AdminGet(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
|
||||
record, err := h.service.repo.GetByID(id)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "file not found"})
|
||||
return
|
||||
}
|
||||
|
||||
c.File(record.Path)
|
||||
}
|
||||
|
||||
func (h *Handler) AdminDelete(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
|
||||
_, err := h.service.DeleteFileByID(id)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "file not found"})
|
||||
return
|
||||
}
|
||||
|
||||
c.Redirect(301, "/admin")
|
||||
}
|
||||
|
||||
func (h *Handler) AdminForceDelete(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
|
||||
_, err := h.service.GetFileByID(id)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "file not found"})
|
||||
return
|
||||
}
|
||||
|
||||
if _, err := h.service.ForceDelete(id); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.Redirect(301, "/admin")
|
||||
}
|
||||
18
internal/file/model.go
Normal file
18
internal/file/model.go
Normal file
@@ -0,0 +1,18 @@
|
||||
package file
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
type FileRecord struct {
|
||||
ID string `gorm:"primaryKey" json:"id"`
|
||||
DeletionID string `json:"deletion_id"`
|
||||
Filename string `json:"filename"`
|
||||
Path string `json:"-"` // file path on disk (not exposed via JSON)
|
||||
ExpiresAt time.Time `json:"expires_at"`
|
||||
DeleteAfterDownload bool `json:"delete_after_download"`
|
||||
Size int64 `json:"size"`
|
||||
DownloadCount int `json:"download_count"`
|
||||
Deleted bool `json:"deleted"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
}
|
||||
86
internal/file/repository.go
Normal file
86
internal/file/repository.go
Normal file
@@ -0,0 +1,86 @@
|
||||
package file
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
var ErrFileNotFound = errors.New("file not found")
|
||||
|
||||
type Repository struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
func NewRepository(db *gorm.DB) *Repository {
|
||||
return &Repository{db: db}
|
||||
}
|
||||
|
||||
func (r *Repository) Create(f *FileRecord) error {
|
||||
return r.db.Create(f).Error
|
||||
}
|
||||
|
||||
func (r *Repository) GetAll() ([]FileRecord, error) {
|
||||
var files []FileRecord
|
||||
if err := r.db.Find(&files).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return files, nil
|
||||
}
|
||||
|
||||
func (r *Repository) GetByID(id string) (*FileRecord, error) {
|
||||
var f FileRecord
|
||||
if err := r.db.First(&f, "id = ?", id).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, ErrFileNotFound
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return &f, nil
|
||||
}
|
||||
|
||||
func (r *Repository) GetPaginated(limit, offset int) ([]FileRecord, int, error) {
|
||||
var files []FileRecord
|
||||
var count int64
|
||||
|
||||
if err := r.db.Model(&FileRecord{}).Count(&count).Error; err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
if err := r.db.
|
||||
Order("created_at DESC").
|
||||
Limit(limit).
|
||||
Offset(offset).
|
||||
Find(&files).Error; err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
return files, int(count), nil
|
||||
}
|
||||
|
||||
func (r *Repository) GetByDeletionID(delID string) (*FileRecord, error) {
|
||||
var f FileRecord
|
||||
if err := r.db.First(&f, "deletion_id = ?", delID).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, ErrFileNotFound
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return &f, nil
|
||||
}
|
||||
|
||||
func (r *Repository) IncrementDownload(f *FileRecord) error {
|
||||
f.DownloadCount++
|
||||
return r.db.Save(f).Error
|
||||
}
|
||||
|
||||
// MarkDeleted Soft delete the record by setting Deleted to true
|
||||
func (r *Repository) MarkDeleted(f *FileRecord) error {
|
||||
f.Deleted = true
|
||||
return r.db.Save(f).Error
|
||||
}
|
||||
|
||||
// Delete Permanently delete the record from the database
|
||||
func (r *Repository) Delete(f *FileRecord) error {
|
||||
return r.db.Delete(f).Error
|
||||
}
|
||||
28
internal/file/routes.go
Normal file
28
internal/file/routes.go
Normal file
@@ -0,0 +1,28 @@
|
||||
package file
|
||||
|
||||
import (
|
||||
"ResendIt/internal/api/middleware"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func RegisterRoutes(r *gin.RouterGroup, h *Handler) {
|
||||
files := r.Group("/files")
|
||||
|
||||
files.POST("/upload", h.Upload)
|
||||
files.GET("/download/:id", h.Download)
|
||||
|
||||
files.GET("/delete/:del_id", h.Delete)
|
||||
|
||||
adminRoutes := files.Group("/")
|
||||
adminRoutes.Use(middleware.AuthMiddleware())
|
||||
adminRoutes.Use(middleware.RequireRole("admin"))
|
||||
|
||||
adminRoutes.GET("/admin", h.AdminList)
|
||||
adminRoutes.GET("/admin/:id", h.AdminGet)
|
||||
|
||||
adminRoutes.GET("/admin/download/:id", h.AdminGet)
|
||||
|
||||
adminRoutes.GET("/admin/delete/:id", h.AdminDelete)
|
||||
adminRoutes.GET("/admin/delete/fr/:id", h.AdminForceDelete)
|
||||
}
|
||||
144
internal/file/service.go
Normal file
144
internal/file/service.go
Normal file
@@ -0,0 +1,144 @@
|
||||
package file
|
||||
|
||||
import (
|
||||
"io"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type Service struct {
|
||||
repo *Repository
|
||||
storageDir string
|
||||
}
|
||||
|
||||
func NewService(r *Repository, storageDir string) *Service {
|
||||
if _, err := os.Stat(storageDir); os.IsNotExist(err) {
|
||||
os.MkdirAll(storageDir, os.ModePerm)
|
||||
}
|
||||
|
||||
return &Service{repo: r, storageDir: storageDir}
|
||||
}
|
||||
|
||||
func (s *Service) UploadFile(filename string, data io.Reader, deleteAfterDownload bool, expiresAfter time.Duration) (*FileRecord, error) {
|
||||
folderID := uuid.NewString()
|
||||
folderPath := s.storageDir + "/" + folderID
|
||||
|
||||
if err := os.MkdirAll(folderPath, os.ModePerm); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
path := folderPath + "/" + filename
|
||||
|
||||
out, err := os.Create(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer out.Close()
|
||||
|
||||
size, err := io.Copy(out, data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
f := &FileRecord{
|
||||
ID: folderID,
|
||||
DeletionID: uuid.NewString(),
|
||||
Filename: filename,
|
||||
Path: path,
|
||||
Size: size,
|
||||
CreatedAt: time.Now(),
|
||||
ExpiresAt: time.Now().Add(expiresAfter),
|
||||
DeleteAfterDownload: deleteAfterDownload,
|
||||
}
|
||||
|
||||
if err := s.repo.Create(f); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return f, nil
|
||||
}
|
||||
|
||||
// DownloadFile Download a file
|
||||
func (s *Service) DownloadFile(id string) (*FileRecord, error) {
|
||||
f, err := s.repo.GetByID(id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if f.Deleted || time.Now().After(f.ExpiresAt) {
|
||||
return nil, ErrFileNotFound
|
||||
}
|
||||
|
||||
_ = s.repo.IncrementDownload(f)
|
||||
|
||||
if f.DeleteAfterDownload {
|
||||
_ = s.repo.MarkDeleted(f)
|
||||
}
|
||||
|
||||
return f, nil
|
||||
}
|
||||
|
||||
func (s *Service) DeleteFileByID(id string) (*FileRecord, error) {
|
||||
f, err := s.repo.GetByID(id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if f.Deleted {
|
||||
return nil, ErrFileNotFound
|
||||
}
|
||||
|
||||
if err := s.repo.MarkDeleted(f); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return f, nil
|
||||
}
|
||||
|
||||
func (s *Service) DeleteFileByDeletionID(delID string) (*FileRecord, error) {
|
||||
f, err := s.repo.GetByDeletionID(delID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if f.Deleted {
|
||||
return nil, ErrFileNotFound
|
||||
}
|
||||
|
||||
if err := s.repo.MarkDeleted(f); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return f, nil
|
||||
}
|
||||
|
||||
func (s *Service) ForceDelete(id string) (*FileRecord, error) {
|
||||
f, err := s.repo.GetByID(id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := os.RemoveAll(s.storageDir + "/" + f.ID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := s.repo.Delete(f); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return f, nil
|
||||
}
|
||||
|
||||
func (s *Service) GetPaginatedFiles(limit, offset int) ([]FileRecord, int, error) {
|
||||
return s.repo.GetPaginated(limit, offset)
|
||||
}
|
||||
|
||||
func (s *Service) GetFileByID(id string) (*FileRecord, error) {
|
||||
return s.repo.GetByID(id)
|
||||
}
|
||||
|
||||
func (s *Service) GetFileByDeletionID(delID string) (*FileRecord, error) {
|
||||
return s.repo.GetByDeletionID(delID)
|
||||
}
|
||||
Reference in New Issue
Block a user