Add video support
This commit is contained in:
@@ -93,7 +93,7 @@ func GetThread(db *sql.DB, threadId int) (Thread, error) {
|
||||
|
||||
func GetPosts(db *sql.DB, threadId int) ([]Post, error) {
|
||||
rows, err := db.Query(`
|
||||
SELECT id, thread_id, author, body, created_at, media_path, ip_hash, number
|
||||
SELECT id, thread_id, author, body, created_at, media_path, ip_hash, number, thumb_path
|
||||
FROM posts
|
||||
WHERE thread_id = ?`, threadId)
|
||||
|
||||
@@ -105,7 +105,7 @@ func GetPosts(db *sql.DB, threadId int) ([]Post, error) {
|
||||
var result []Post
|
||||
for rows.Next() {
|
||||
var p Post
|
||||
err := rows.Scan(&p.Id, &p.ThreadId, &p.Author, &p.Body, &p.CreatedAt, &p.MediaPath, &p.IpHash, &p.Number)
|
||||
err := rows.Scan(&p.Id, &p.ThreadId, &p.Author, &p.Body, &p.CreatedAt, &p.MediaPath, &p.IpHash, &p.Number, &p.ThumbPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -117,20 +117,20 @@ func GetPosts(db *sql.DB, threadId int) ([]Post, error) {
|
||||
|
||||
func GetOriginalPost(db *sql.DB, threadId int) (Post, error) {
|
||||
row := db.QueryRow(`
|
||||
SELECT id, thread_id, author, body, created_at, media_path, ip_hash, number
|
||||
SELECT id, thread_id, author, body, created_at, media_path, ip_hash, number, thumb_path
|
||||
FROM posts
|
||||
WHERE thread_id = ?
|
||||
ORDER BY created_at ASC LIMIT 1`, threadId)
|
||||
|
||||
var r Post
|
||||
err := row.Scan(&r.Id, &r.ThreadId, &r.Author, &r.Body, &r.CreatedAt, &r.MediaPath, &r.IpHash, &r.Number)
|
||||
err := row.Scan(&r.Id, &r.ThreadId, &r.Author, &r.Body, &r.CreatedAt, &r.MediaPath, &r.IpHash, &r.Number, &r.ThumbPath)
|
||||
if err != nil {
|
||||
return Post{}, err
|
||||
}
|
||||
return r, row.Err()
|
||||
}
|
||||
|
||||
func PutThread(db *sql.DB, boardSlug string, subject string, body string, mediaPath string, ip_hash string) error {
|
||||
func PutThread(db *sql.DB, boardSlug string, subject string, body string, mediaPath string, thumbPath string, ip_hash string) error {
|
||||
tx, err := db.Begin()
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -150,7 +150,7 @@ func PutThread(db *sql.DB, boardSlug string, subject string, body string, mediaP
|
||||
return err
|
||||
}
|
||||
|
||||
if err := PutPost(tx, boardSlug, int(threadId), body, mediaPath, ip_hash); err != nil {
|
||||
if err := PutPost(tx, boardSlug, int(threadId), body, mediaPath, thumbPath, ip_hash); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -188,7 +188,7 @@ func PutThread(db *sql.DB, boardSlug string, subject string, body string, mediaP
|
||||
return nil
|
||||
}
|
||||
|
||||
func PutPost(db Queryer, boardSlug string, threadId int, body string, mediaPath string, ip_hash string) error {
|
||||
func PutPost(db Queryer, boardSlug string, threadId int, body string, mediaPath string, thumbPath string, ip_hash string) error {
|
||||
row := db.QueryRow(`
|
||||
SELECT MAX(p.number)
|
||||
FROM posts p
|
||||
@@ -206,8 +206,8 @@ func PutPost(db Queryer, boardSlug string, threadId int, body string, mediaPath
|
||||
}
|
||||
|
||||
_, err := db.Exec(`
|
||||
INSERT INTO posts (thread_id, body, media_path, ip_hash, number)
|
||||
VALUES (?, ?, ?, ?, ?)`, threadId, body, mediaPath, ip_hash, newPostNumber)
|
||||
INSERT INTO posts (thread_id, body, media_path, ip_hash, number, thumb_path)
|
||||
VALUES (?, ?, ?, ?, ?, ?)`, threadId, body, mediaPath, ip_hash, newPostNumber, thumbPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -241,10 +241,10 @@ func DeleteThread(db Queryer, threadId int) error {
|
||||
}
|
||||
|
||||
for _, pruneMediaPath := range pruneMediaPaths {
|
||||
if err := os.Remove(path.Join(util.POST_IMG_FULL_PATH, pruneMediaPath)); err != nil && !os.IsNotExist(err) {
|
||||
if err := os.Remove(path.Join(util.POST_MEDIA_FULL_PATH, pruneMediaPath)); err != nil && !os.IsNotExist(err) {
|
||||
return err
|
||||
}
|
||||
if err := os.Remove(path.Join(util.POST_IMG_THUMB_PATH, pruneMediaPath)); err != nil && !os.IsNotExist(err) {
|
||||
if err := os.Remove(path.Join(util.POST_MEDIA_THUMB_PATH, pruneMediaPath)); err != nil && !os.IsNotExist(err) {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@ type Post struct {
|
||||
Body string
|
||||
CreatedAt time.Time
|
||||
MediaPath string
|
||||
ThumbPath string
|
||||
IpHash string
|
||||
Number int
|
||||
}
|
||||
|
||||
@@ -23,11 +23,12 @@ CREATE TABLE IF NOT EXISTS threads (
|
||||
CREATE TABLE IF NOT EXISTS posts (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
thread_id INTEGER NOT NULL,
|
||||
number INTEGER NOT NULL,
|
||||
number INTEGER NOT NULL ,
|
||||
author TEXT DEFAULT 'Anonymous',
|
||||
body TEXT NOT NULL,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
media_path TEXT NOT NULL DEFAULT '',
|
||||
thumb_path TEXT NOT NULL DEFAULT '',
|
||||
ip_hash TEXT NOT NULL,
|
||||
FOREIGN KEY (thread_id) REFERENCES threads(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
+113
-45
@@ -7,7 +7,9 @@ import (
|
||||
"io"
|
||||
"log"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
@@ -17,10 +19,12 @@ import (
|
||||
|
||||
// 10 MB memory limit
|
||||
const FILE_MEM_LIMIT int64 = 10 << 20
|
||||
const POST_IMG_FULL_PATH = "web/static/img/posts/full"
|
||||
const POST_IMG_THUMB_PATH = "web/static/img/posts/thumb"
|
||||
const POST_MEDIA_FULL_PATH = "web/static/media/posts/full"
|
||||
const POST_MEDIA_THUMB_PATH = "web/static/media/posts/thumb"
|
||||
const MAX_THREAD_COUNT = 50
|
||||
|
||||
var SUPPORTED_VID_FORMATS = []string{".mp4", ".webm", ".ogg"}
|
||||
|
||||
func EnrichPost(body string) string {
|
||||
var b strings.Builder
|
||||
for _, rawLine := range strings.Split(body, "\n") {
|
||||
@@ -58,86 +62,142 @@ func EnrichPost(body string) string {
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func SavePostFile(file *multipart.File, filename string) error {
|
||||
dstPathFull := filepath.Join(POST_IMG_FULL_PATH, filename)
|
||||
dstPathThumb := filepath.Join(POST_IMG_THUMB_PATH, filename)
|
||||
func SavePostFile(file multipart.File, fileName string) (error, string, string) {
|
||||
// read file type
|
||||
buffer := make([]byte, 512)
|
||||
file.Read(buffer)
|
||||
file.Seek(0, 0)
|
||||
|
||||
fileType := http.DetectContentType(buffer)
|
||||
isFileVideo := strings.HasPrefix(fileType, "video/")
|
||||
fileExt := strings.ToLower(filepath.Ext(fileName))
|
||||
|
||||
// FULL
|
||||
if err := os.MkdirAll(POST_IMG_FULL_PATH, 0755); err != nil {
|
||||
if err := os.MkdirAll(POST_MEDIA_FULL_PATH, 0755); err != nil {
|
||||
log.Printf("MkdirAll (full): %v", err)
|
||||
return err
|
||||
return err, "", ""
|
||||
}
|
||||
|
||||
dstPathFull := filepath.Join(POST_MEDIA_FULL_PATH, fileName)
|
||||
dstFull, err := os.Create(dstPathFull)
|
||||
if err != nil {
|
||||
log.Printf("os.Create (full): %v", err)
|
||||
return err
|
||||
return err, "", ""
|
||||
}
|
||||
defer dstFull.Close()
|
||||
if _, err := io.Copy(dstFull, *file); err != nil {
|
||||
if _, err := io.Copy(dstFull, file); err != nil {
|
||||
log.Printf("io.Copy (full): %v", err)
|
||||
return err
|
||||
return err, "", ""
|
||||
}
|
||||
|
||||
if _, err := (*file).Seek(0, 0); err != nil { // rewind file
|
||||
if _, err := (file).Seek(0, 0); err != nil { // rewind file
|
||||
log.Printf("seek file (thumb): %v", err)
|
||||
return err
|
||||
return err, "", ""
|
||||
}
|
||||
|
||||
// THUMBNAIL
|
||||
if err := os.MkdirAll(POST_IMG_THUMB_PATH, 0755); err != nil {
|
||||
dstPathThumb := filepath.Join(POST_MEDIA_THUMB_PATH, fileName)
|
||||
if err := os.MkdirAll(POST_MEDIA_THUMB_PATH, 0755); err != nil {
|
||||
log.Printf("MkdirAll (thumb): %v", err)
|
||||
return err
|
||||
return err, "", ""
|
||||
}
|
||||
|
||||
img, _, err := image.Decode(*file)
|
||||
if err != nil {
|
||||
log.Printf("image.Decode: %v", err)
|
||||
return err
|
||||
var thumbFileName string
|
||||
if isFileVideo {
|
||||
fileNameNoExt := strings.TrimSuffix(fileName, fileExt)
|
||||
thumbFileName = fileNameNoExt + ".jpg"
|
||||
|
||||
inputPath := filepath.Join(POST_MEDIA_FULL_PATH, fileName)
|
||||
outputPath := filepath.Join(POST_MEDIA_THUMB_PATH, thumbFileName)
|
||||
|
||||
cmd := exec.Command(
|
||||
"ffmpeg",
|
||||
"-i", inputPath,
|
||||
"-ss", "00:00:01.000",
|
||||
"-vframes", "1",
|
||||
"-vf", "scale=300:-1",
|
||||
outputPath,
|
||||
)
|
||||
|
||||
if err := cmd.Run(); err != nil {
|
||||
log.Printf("ffmpeg error: %v", err)
|
||||
return err, "", ""
|
||||
}
|
||||
|
||||
} else {
|
||||
thumbFileName = fileName
|
||||
|
||||
img, _, err := image.Decode(file)
|
||||
if err != nil {
|
||||
log.Printf("image.Decode: %v", err)
|
||||
return err, "", ""
|
||||
}
|
||||
|
||||
var thumb image.Image
|
||||
if img.Bounds().Dx() > 300 {
|
||||
thumb = imaging.Resize(img, 300, 0, imaging.Lanczos)
|
||||
} else {
|
||||
thumb = img
|
||||
}
|
||||
|
||||
if err = imaging.Save(thumb, dstPathThumb); err != nil {
|
||||
log.Printf("imaging.Save: %v", err)
|
||||
return err, "", ""
|
||||
}
|
||||
}
|
||||
|
||||
thumb := imaging.Resize(img, 300, 0, imaging.Lanczos)
|
||||
if err = imaging.Save(thumb, dstPathThumb); err != nil {
|
||||
log.Printf("imaging.Save: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
return nil, fileName, thumbFileName
|
||||
}
|
||||
|
||||
type PostImageInfo struct {
|
||||
Size int64
|
||||
Height int
|
||||
Width int
|
||||
type PostFileInfo struct {
|
||||
Size int64
|
||||
Height int
|
||||
Width int
|
||||
IsVideo bool
|
||||
}
|
||||
|
||||
func GetPostImageInfo(mediaPath string) PostImageInfo {
|
||||
var result PostImageInfo
|
||||
func GetPostFileInfo(mediaPath string) PostFileInfo {
|
||||
var result PostFileInfo
|
||||
|
||||
imagePath := path.Join(POST_IMG_FULL_PATH, mediaPath)
|
||||
filePath := path.Join(POST_MEDIA_FULL_PATH, mediaPath)
|
||||
|
||||
fileInfo, err := os.Stat(imagePath)
|
||||
fileInfo, err := os.Stat(filePath)
|
||||
if err != nil {
|
||||
log.Printf("Failed to get image file info at %s: %v", mediaPath, err)
|
||||
return PostImageInfo{}
|
||||
log.Printf("Failed to get file info at %s: %v", mediaPath, err)
|
||||
return PostFileInfo{}
|
||||
}
|
||||
|
||||
file, err := os.Open(imagePath)
|
||||
file, err := os.Open(filePath)
|
||||
if err != nil {
|
||||
log.Printf("Failed to open image at %s: %v", mediaPath, err)
|
||||
return PostImageInfo{}
|
||||
log.Printf("Failed to open file at %s: %v", mediaPath, err)
|
||||
return PostFileInfo{}
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
cfg, _, err := image.DecodeConfig(file)
|
||||
if err != nil {
|
||||
log.Printf("Failed to decode image at %s: %v", mediaPath, err)
|
||||
return PostImageInfo{}
|
||||
}
|
||||
// read file type
|
||||
buffer := make([]byte, 512)
|
||||
file.Read(buffer)
|
||||
file.Seek(0, 0)
|
||||
|
||||
fileType := http.DetectContentType(buffer)
|
||||
isFileVideo := strings.HasPrefix(fileType, "video/")
|
||||
|
||||
result.Size = fileInfo.Size()
|
||||
result.Width = cfg.Width
|
||||
result.Height = cfg.Height
|
||||
|
||||
if !isFileVideo {
|
||||
cfg, _, err := image.DecodeConfig(file)
|
||||
if err != nil {
|
||||
log.Printf("Failed to decode image at %s: %v", mediaPath, err)
|
||||
return PostFileInfo{}
|
||||
}
|
||||
|
||||
result.Width = cfg.Width
|
||||
result.Height = cfg.Height
|
||||
result.IsVideo = false
|
||||
|
||||
} else {
|
||||
result.IsVideo = true
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
@@ -163,3 +223,11 @@ func FormatBytes(bytes int64) string {
|
||||
return fmt.Sprintf("%d B", bytes)
|
||||
}
|
||||
}
|
||||
|
||||
func FormatFileInfo(fileInfo PostFileInfo) string {
|
||||
humanSize := FormatBytes(fileInfo.Size)
|
||||
if fileInfo.IsVideo {
|
||||
return fmt.Sprintf("(%s)", humanSize)
|
||||
}
|
||||
return fmt.Sprintf("(%s, %dx%d)", humanSize, fileInfo.Width, fileInfo.Height)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user