Add video support
This commit is contained in:
+2
-2
@@ -2,5 +2,5 @@ internal/database/comfychan.db
|
|||||||
**/*_templ.go
|
**/*_templ.go
|
||||||
tmp
|
tmp
|
||||||
|
|
||||||
web/static/img/posts/
|
web/static/media/posts/
|
||||||
web/static/img/posts/
|
web/static/media/posts/
|
||||||
|
|||||||
@@ -20,4 +20,4 @@ db/seed/f:
|
|||||||
rm ./internal/database/comfychan.db && sqlite3 ./internal/database/comfychan.db < ./internal/database/seed.sql
|
rm ./internal/database/comfychan.db && sqlite3 ./internal/database/comfychan.db < ./internal/database/seed.sql
|
||||||
|
|
||||||
img/clear:
|
img/clear:
|
||||||
rm -rf ./web/static/img/posts || true
|
rm -rf ./web/static/media/posts || true
|
||||||
|
|||||||
@@ -93,7 +93,7 @@ func GetThread(db *sql.DB, threadId int) (Thread, error) {
|
|||||||
|
|
||||||
func GetPosts(db *sql.DB, threadId int) ([]Post, error) {
|
func GetPosts(db *sql.DB, threadId int) ([]Post, error) {
|
||||||
rows, err := db.Query(`
|
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
|
FROM posts
|
||||||
WHERE thread_id = ?`, threadId)
|
WHERE thread_id = ?`, threadId)
|
||||||
|
|
||||||
@@ -105,7 +105,7 @@ func GetPosts(db *sql.DB, threadId int) ([]Post, error) {
|
|||||||
var result []Post
|
var result []Post
|
||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
var p Post
|
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 {
|
if err != nil {
|
||||||
return nil, err
|
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) {
|
func GetOriginalPost(db *sql.DB, threadId int) (Post, error) {
|
||||||
row := db.QueryRow(`
|
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
|
FROM posts
|
||||||
WHERE thread_id = ?
|
WHERE thread_id = ?
|
||||||
ORDER BY created_at ASC LIMIT 1`, threadId)
|
ORDER BY created_at ASC LIMIT 1`, threadId)
|
||||||
|
|
||||||
var r Post
|
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 {
|
if err != nil {
|
||||||
return Post{}, err
|
return Post{}, err
|
||||||
}
|
}
|
||||||
return r, row.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()
|
tx, err := db.Begin()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -150,7 +150,7 @@ func PutThread(db *sql.DB, boardSlug string, subject string, body string, mediaP
|
|||||||
return err
|
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
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -188,7 +188,7 @@ func PutThread(db *sql.DB, boardSlug string, subject string, body string, mediaP
|
|||||||
return nil
|
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(`
|
row := db.QueryRow(`
|
||||||
SELECT MAX(p.number)
|
SELECT MAX(p.number)
|
||||||
FROM posts p
|
FROM posts p
|
||||||
@@ -206,8 +206,8 @@ func PutPost(db Queryer, boardSlug string, threadId int, body string, mediaPath
|
|||||||
}
|
}
|
||||||
|
|
||||||
_, err := db.Exec(`
|
_, err := db.Exec(`
|
||||||
INSERT INTO posts (thread_id, body, media_path, ip_hash, number)
|
INSERT INTO posts (thread_id, body, media_path, ip_hash, number, thumb_path)
|
||||||
VALUES (?, ?, ?, ?, ?)`, threadId, body, mediaPath, ip_hash, newPostNumber)
|
VALUES (?, ?, ?, ?, ?, ?)`, threadId, body, mediaPath, ip_hash, newPostNumber, thumbPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -241,10 +241,10 @@ func DeleteThread(db Queryer, threadId int) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for _, pruneMediaPath := range pruneMediaPaths {
|
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
|
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
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ type Post struct {
|
|||||||
Body string
|
Body string
|
||||||
CreatedAt time.Time
|
CreatedAt time.Time
|
||||||
MediaPath string
|
MediaPath string
|
||||||
|
ThumbPath string
|
||||||
IpHash string
|
IpHash string
|
||||||
Number int
|
Number int
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ CREATE TABLE IF NOT EXISTS posts (
|
|||||||
body TEXT NOT NULL,
|
body TEXT NOT NULL,
|
||||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
media_path TEXT NOT NULL DEFAULT '',
|
media_path TEXT NOT NULL DEFAULT '',
|
||||||
|
thumb_path TEXT NOT NULL DEFAULT '',
|
||||||
ip_hash TEXT NOT NULL,
|
ip_hash TEXT NOT NULL,
|
||||||
FOREIGN KEY (thread_id) REFERENCES threads(id) ON DELETE CASCADE
|
FOREIGN KEY (thread_id) REFERENCES threads(id) ON DELETE CASCADE
|
||||||
);
|
);
|
||||||
|
|||||||
+99
-31
@@ -7,7 +7,9 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"log"
|
"log"
|
||||||
"mime/multipart"
|
"mime/multipart"
|
||||||
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
|
"os/exec"
|
||||||
"path"
|
"path"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -17,10 +19,12 @@ import (
|
|||||||
|
|
||||||
// 10 MB memory limit
|
// 10 MB memory limit
|
||||||
const FILE_MEM_LIMIT int64 = 10 << 20
|
const FILE_MEM_LIMIT int64 = 10 << 20
|
||||||
const POST_IMG_FULL_PATH = "web/static/img/posts/full"
|
const POST_MEDIA_FULL_PATH = "web/static/media/posts/full"
|
||||||
const POST_IMG_THUMB_PATH = "web/static/img/posts/thumb"
|
const POST_MEDIA_THUMB_PATH = "web/static/media/posts/thumb"
|
||||||
const MAX_THREAD_COUNT = 50
|
const MAX_THREAD_COUNT = 50
|
||||||
|
|
||||||
|
var SUPPORTED_VID_FORMATS = []string{".mp4", ".webm", ".ogg"}
|
||||||
|
|
||||||
func EnrichPost(body string) string {
|
func EnrichPost(body string) string {
|
||||||
var b strings.Builder
|
var b strings.Builder
|
||||||
for _, rawLine := range strings.Split(body, "\n") {
|
for _, rawLine := range strings.Split(body, "\n") {
|
||||||
@@ -58,86 +62,142 @@ func EnrichPost(body string) string {
|
|||||||
return b.String()
|
return b.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
func SavePostFile(file *multipart.File, filename string) error {
|
func SavePostFile(file multipart.File, fileName string) (error, string, string) {
|
||||||
dstPathFull := filepath.Join(POST_IMG_FULL_PATH, filename)
|
// read file type
|
||||||
dstPathThumb := filepath.Join(POST_IMG_THUMB_PATH, filename)
|
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
|
// 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)
|
log.Printf("MkdirAll (full): %v", err)
|
||||||
return err
|
return err, "", ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
dstPathFull := filepath.Join(POST_MEDIA_FULL_PATH, fileName)
|
||||||
dstFull, err := os.Create(dstPathFull)
|
dstFull, err := os.Create(dstPathFull)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("os.Create (full): %v", err)
|
log.Printf("os.Create (full): %v", err)
|
||||||
return err
|
return err, "", ""
|
||||||
}
|
}
|
||||||
defer dstFull.Close()
|
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)
|
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)
|
log.Printf("seek file (thumb): %v", err)
|
||||||
return err
|
return err, "", ""
|
||||||
}
|
}
|
||||||
|
|
||||||
// THUMBNAIL
|
// 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)
|
log.Printf("MkdirAll (thumb): %v", err)
|
||||||
return err
|
return err, "", ""
|
||||||
}
|
}
|
||||||
|
|
||||||
img, _, err := image.Decode(*file)
|
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 {
|
if err != nil {
|
||||||
log.Printf("image.Decode: %v", err)
|
log.Printf("image.Decode: %v", err)
|
||||||
return err
|
return err, "", ""
|
||||||
|
}
|
||||||
|
|
||||||
|
var thumb image.Image
|
||||||
|
if img.Bounds().Dx() > 300 {
|
||||||
|
thumb = imaging.Resize(img, 300, 0, imaging.Lanczos)
|
||||||
|
} else {
|
||||||
|
thumb = img
|
||||||
}
|
}
|
||||||
|
|
||||||
thumb := imaging.Resize(img, 300, 0, imaging.Lanczos)
|
|
||||||
if err = imaging.Save(thumb, dstPathThumb); err != nil {
|
if err = imaging.Save(thumb, dstPathThumb); err != nil {
|
||||||
log.Printf("imaging.Save: %v", err)
|
log.Printf("imaging.Save: %v", err)
|
||||||
return err
|
return err, "", ""
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil, fileName, thumbFileName
|
||||||
}
|
}
|
||||||
|
|
||||||
type PostImageInfo struct {
|
type PostFileInfo struct {
|
||||||
Size int64
|
Size int64
|
||||||
Height int
|
Height int
|
||||||
Width int
|
Width int
|
||||||
|
IsVideo bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetPostImageInfo(mediaPath string) PostImageInfo {
|
func GetPostFileInfo(mediaPath string) PostFileInfo {
|
||||||
var result PostImageInfo
|
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 {
|
if err != nil {
|
||||||
log.Printf("Failed to get image file info at %s: %v", mediaPath, err)
|
log.Printf("Failed to get file info at %s: %v", mediaPath, err)
|
||||||
return PostImageInfo{}
|
return PostFileInfo{}
|
||||||
}
|
}
|
||||||
|
|
||||||
file, err := os.Open(imagePath)
|
file, err := os.Open(filePath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Failed to open image at %s: %v", mediaPath, err)
|
log.Printf("Failed to open file at %s: %v", mediaPath, err)
|
||||||
return PostImageInfo{}
|
return PostFileInfo{}
|
||||||
}
|
}
|
||||||
defer file.Close()
|
defer file.Close()
|
||||||
|
|
||||||
|
// 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()
|
||||||
|
|
||||||
|
if !isFileVideo {
|
||||||
cfg, _, err := image.DecodeConfig(file)
|
cfg, _, err := image.DecodeConfig(file)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Failed to decode image at %s: %v", mediaPath, err)
|
log.Printf("Failed to decode image at %s: %v", mediaPath, err)
|
||||||
return PostImageInfo{}
|
return PostFileInfo{}
|
||||||
}
|
}
|
||||||
|
|
||||||
result.Size = fileInfo.Size()
|
|
||||||
result.Width = cfg.Width
|
result.Width = cfg.Width
|
||||||
result.Height = cfg.Height
|
result.Height = cfg.Height
|
||||||
|
result.IsVideo = false
|
||||||
|
|
||||||
|
} else {
|
||||||
|
result.IsVideo = true
|
||||||
|
}
|
||||||
|
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
@@ -163,3 +223,11 @@ func FormatBytes(bytes int64) string {
|
|||||||
return fmt.Sprintf("%d B", bytes)
|
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)
|
||||||
|
}
|
||||||
|
|||||||
+37
-15
@@ -7,6 +7,7 @@ import (
|
|||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"slices"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
@@ -155,13 +156,14 @@ func main() {
|
|||||||
defer file.Close()
|
defer file.Close()
|
||||||
|
|
||||||
filename := strconv.FormatInt(time.Now().UnixNano(), 10) + filepath.Ext(header.Filename)
|
filename := strconv.FormatInt(time.Now().UnixNano(), 10) + filepath.Ext(header.Filename)
|
||||||
if err := util.SavePostFile(&file, filename); err != nil {
|
err, savedMediaPath, savedThumbPath := util.SavePostFile(file, filename)
|
||||||
|
if err != nil {
|
||||||
http.Error(w, "Failed to save file", http.StatusInternalServerError)
|
http.Error(w, "Failed to save file", http.StatusInternalServerError)
|
||||||
log.Printf("savePostFile: %v", err)
|
log.Printf("savePostFile: %v", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := database.PutThread(db, slug, subject, body, filename, util.HashIp(ip)); err != nil {
|
if err := database.PutThread(db, slug, subject, body, savedMediaPath, savedThumbPath, util.HashIp(ip)); err != nil {
|
||||||
http.Error(w, "Failed to create thread", http.StatusInternalServerError)
|
http.Error(w, "Failed to create thread", http.StatusInternalServerError)
|
||||||
log.Printf("PutThread: %v", err)
|
log.Printf("PutThread: %v", err)
|
||||||
return
|
return
|
||||||
@@ -180,13 +182,6 @@ func main() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
threadIdStr := chi.URLParam(r, "threadId")
|
|
||||||
threadId, err := strconv.Atoi(threadIdStr)
|
|
||||||
if err != nil {
|
|
||||||
http.Error(w, "Invalid thread id", http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := r.ParseMultipartForm(util.FILE_MEM_LIMIT); err != nil {
|
if err := r.ParseMultipartForm(util.FILE_MEM_LIMIT); err != nil {
|
||||||
http.Error(w, "Failed to parse form", http.StatusBadRequest)
|
http.Error(w, "Failed to parse form", http.StatusBadRequest)
|
||||||
log.Printf("ParseMultipartForm: %v", err)
|
log.Printf("ParseMultipartForm: %v", err)
|
||||||
@@ -195,6 +190,7 @@ func main() {
|
|||||||
|
|
||||||
body := strings.TrimSpace(r.FormValue("body"))
|
body := strings.TrimSpace(r.FormValue("body"))
|
||||||
mediaPath := ""
|
mediaPath := ""
|
||||||
|
thumbPath := ""
|
||||||
|
|
||||||
if body == "" {
|
if body == "" {
|
||||||
http.Error(w, "Malformed post body", http.StatusBadRequest)
|
http.Error(w, "Malformed post body", http.StatusBadRequest)
|
||||||
@@ -204,22 +200,48 @@ func main() {
|
|||||||
file, header, err := r.FormFile("file")
|
file, header, err := r.FormFile("file")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if !errors.Is(err, http.ErrMissingFile) {
|
if !errors.Is(err, http.ErrMissingFile) {
|
||||||
http.Error(w, "Failed to retrive file from form", http.StatusBadRequest)
|
http.Error(w, "Failed to retrieve file from form", http.StatusBadRequest)
|
||||||
log.Printf("FormFile: %v", err)
|
log.Printf("FormFile: %v", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
defer file.Close()
|
defer file.Close()
|
||||||
filename := strconv.FormatInt(time.Now().UnixNano(), 10) + filepath.Ext(header.Filename)
|
|
||||||
if err := util.SavePostFile(&file, filename); err != nil {
|
isFileVideo := false
|
||||||
|
fileExt := strings.ToLower(filepath.Ext(header.Filename))
|
||||||
|
|
||||||
|
// file type
|
||||||
|
buffer := make([]byte, 512)
|
||||||
|
file.Read(buffer)
|
||||||
|
file.Seek(0, 0)
|
||||||
|
fileType := http.DetectContentType(buffer)
|
||||||
|
|
||||||
|
isFileVideo = strings.HasPrefix(fileType, "video/")
|
||||||
|
|
||||||
|
if isFileVideo && !slices.Contains(util.SUPPORTED_VID_FORMATS, fileExt) {
|
||||||
|
http.Error(w, "Unsupported file format", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
filename := strconv.FormatInt(time.Now().UnixNano(), 10) + fileExt
|
||||||
|
err, savedMediaPath, savedThumbPath := util.SavePostFile(file, filename)
|
||||||
|
if err != nil {
|
||||||
http.Error(w, "Failed to save file", http.StatusInternalServerError)
|
http.Error(w, "Failed to save file", http.StatusInternalServerError)
|
||||||
log.Printf("savePostFile: %v", err)
|
log.Printf("savePostFile: %v", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
mediaPath = filename
|
mediaPath = savedMediaPath
|
||||||
|
thumbPath = savedThumbPath
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := database.PutPost(db, slug, threadId, body, mediaPath, util.HashIp(ip)); err != nil {
|
threadIdStr := chi.URLParam(r, "threadId")
|
||||||
|
threadId, err := strconv.Atoi(threadIdStr)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "Invalid thread id", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := database.PutPost(db, slug, threadId, body, mediaPath, thumbPath, util.HashIp(ip)); err != nil {
|
||||||
http.Error(w, "Failed to create post", http.StatusInternalServerError)
|
http.Error(w, "Failed to create post", http.StatusInternalServerError)
|
||||||
log.Printf("PutPost: %v", err)
|
log.Printf("PutPost: %v", err)
|
||||||
return
|
return
|
||||||
@@ -262,7 +284,7 @@ func main() {
|
|||||||
Subject: thread.Subject,
|
Subject: thread.Subject,
|
||||||
Body: op.Body,
|
Body: op.Body,
|
||||||
ThreadURL: fmt.Sprintf("/%s/threads/%d", slug, thread.Id),
|
ThreadURL: fmt.Sprintf("/%s/threads/%d", slug, thread.Id),
|
||||||
MediaPath: op.MediaPath,
|
ThumbPath: op.ThumbPath,
|
||||||
ReplyCount: len(posts),
|
ReplyCount: len(posts),
|
||||||
IpCount: SumUniquePostIps(posts),
|
IpCount: SumUniquePostIps(posts),
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -220,6 +220,14 @@ body {
|
|||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.post-vid {
|
||||||
|
display: block;
|
||||||
|
float: left;
|
||||||
|
margin: 4px 12px 4px 0;
|
||||||
|
height: auto;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
.post-img-info {
|
.post-img-info {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 213 KiB After Width: | Height: | Size: 213 KiB |
|
Before Width: | Height: | Size: 65 KiB After Width: | Height: | Size: 65 KiB |
|
Before Width: | Height: | Size: 3.4 MiB After Width: | Height: | Size: 3.4 MiB |
+24
-7
@@ -36,15 +36,32 @@ function insertHeaderReplies() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function togglePostImage(img) {
|
function togglePostFile(imgEl) {
|
||||||
const filename = img.src.split("/").at(-1);
|
const isVideo = imgEl.dataset.full.endsWith(".mp4") || imgEl.dataset.full.endsWith(".webm") || imgEl.dataset.full.endsWith(".ogg");
|
||||||
if (img.classList.contains("post-img-full")) {
|
|
||||||
img.classList.remove("post-img-full");
|
|
||||||
img.src = "/static/img/posts/thumb/" + filename;
|
if (isVideo) {
|
||||||
|
const vidEl = imgEl.parentElement.querySelector("video");
|
||||||
|
if (vidEl.style.display === "none") {
|
||||||
|
vidEl.src = "/static/media/posts/full/" + imgEl.dataset.full;
|
||||||
|
vidEl.style.display = "";
|
||||||
|
imgEl.style.display = "none";
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
img.classList.add("post-img-full");
|
vidEl.src = "";
|
||||||
img.src = "/static/img/posts/full/" + filename;
|
vidEl.style.display = "none";
|
||||||
|
imgEl.style.display = "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
if (imgEl.classList.contains("post-img-full")) {
|
||||||
|
imgEl.classList.remove("post-img-full");
|
||||||
|
imgEl.src = "/static/media/posts/thumb/" + imgEl.dataset.thumb;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
imgEl.classList.add("post-img-full");
|
||||||
|
imgEl.src = "/static/media/posts/full/" + imgEl.dataset.full;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+13
-5
@@ -42,7 +42,15 @@ templ NewThreadForm(board database.Board) {
|
|||||||
</tr>
|
</tr>
|
||||||
<tr class="new-post-form-field">
|
<tr class="new-post-form-field">
|
||||||
<th>File</th>
|
<th>File</th>
|
||||||
<td><input id="newPostFile" name="file" type="file" required/></td>
|
<td>
|
||||||
|
<input
|
||||||
|
accept="image/*,video/mp4,video/webm,video/ogg"
|
||||||
|
id="newPostFile"
|
||||||
|
name="file"
|
||||||
|
type="file"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
@@ -52,7 +60,7 @@ templ NewThreadForm(board database.Board) {
|
|||||||
|
|
||||||
templ BoardHeader(board database.Board) {
|
templ BoardHeader(board database.Board) {
|
||||||
<header class="board-header">
|
<header class="board-header">
|
||||||
<img src={ fmt.Sprintf("/static/img/banners/%s.png", board.Slug) }/>
|
<img src={ fmt.Sprintf("/static/media/banners/%s.png", board.Slug) }/>
|
||||||
<h1>
|
<h1>
|
||||||
{ fmt.Sprintf("/%s/ - %s", board.Slug, board.Name) }
|
{ fmt.Sprintf("/%s/ - %s", board.Slug, board.Name) }
|
||||||
</h1>
|
</h1>
|
||||||
@@ -91,7 +99,7 @@ type CatalogThreadPreview struct {
|
|||||||
Subject string
|
Subject string
|
||||||
Body string
|
Body string
|
||||||
ThreadURL string
|
ThreadURL string
|
||||||
MediaPath string
|
ThumbPath string
|
||||||
ReplyCount int
|
ReplyCount int
|
||||||
IpCount int
|
IpCount int
|
||||||
}
|
}
|
||||||
@@ -106,8 +114,8 @@ templ ThreadsCatalog(previews []CatalogThreadPreview) {
|
|||||||
<img
|
<img
|
||||||
loading="lazy"
|
loading="lazy"
|
||||||
class="catalog-preview-img"
|
class="catalog-preview-img"
|
||||||
src={ fmt.Sprintf("/static/img/posts/thumb/%s",
|
src={ fmt.Sprintf("/static/media/posts/thumb/%s",
|
||||||
preview.MediaPath) }
|
preview.ThumbPath) }
|
||||||
/>
|
/>
|
||||||
</a>
|
</a>
|
||||||
<strong class="catalog-preview-counts">R: { replyCount } / I: { ipCount }</strong>
|
<strong class="catalog-preview-counts">R: { replyCount } / I: { ipCount }</strong>
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import "github.com/dominicf2001/comfychan/web/views/shared"
|
|||||||
templ Index() {
|
templ Index() {
|
||||||
@shared.Layout() {
|
@shared.Layout() {
|
||||||
<section class="container">
|
<section class="container">
|
||||||
<img id="clavis" src="/static/img/clavis.png"/>
|
<img id="clavis" src="/static/media/clavis.png"/>
|
||||||
<div class="box">
|
<div class="box">
|
||||||
<h2>Welcome to Comfychan</h2>
|
<h2>Welcome to Comfychan</h2>
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
+23
-14
@@ -9,24 +9,25 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
templ PostOriginal(post database.Post, thread *database.Thread) {
|
templ PostOriginal(post database.Post, thread *database.Thread) {
|
||||||
{{ imgInfo := util.GetPostImageInfo(post.MediaPath) }}
|
|
||||||
<article id={ fmt.Sprintf("post-%d", post.Number) } class="post-op">
|
<article id={ fmt.Sprintf("post-%d", post.Number) } class="post-op">
|
||||||
<div>
|
<div>
|
||||||
File:
|
File:
|
||||||
<a href={ templ.URL("/static/img/posts/full/" + post.MediaPath) } class="post-filename">
|
<a href={ templ.URL("/static/media/posts/full/" + post.MediaPath) } class="post-filename">
|
||||||
{ post.MediaPath }
|
{ post.MediaPath }
|
||||||
</a>
|
</a>
|
||||||
<div class="post-img-info">
|
<div class="post-img-info">
|
||||||
(<span>{ util.FormatBytes(imgInfo.Size) }</span>,
|
<span>{ util.FormatFileInfo(util.GetPostFileInfo(post.MediaPath)) }</span>
|
||||||
<span>{ strconv.Itoa(imgInfo.Width) }</span>x<span>{ strconv.Itoa(imgInfo.Height) }</span>)
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<img
|
<img
|
||||||
onclick="togglePostImage(this)"
|
onclick="togglePostFile(this)"
|
||||||
loading="lazy"
|
loading="lazy"
|
||||||
src={ fmt.Sprintf("/static/img/posts/thumb/%s", post.MediaPath) }
|
src={ fmt.Sprintf("/static/media/posts/thumb/%s", post.ThumbPath) }
|
||||||
|
data-full={ post.MediaPath }
|
||||||
|
data-thumb={ post.ThumbPath }
|
||||||
class="post-img"
|
class="post-img"
|
||||||
/>
|
/>
|
||||||
|
<video controls style="display: none;" class="post-vid"></video>
|
||||||
<header style="margin-top: 10px;" class="post-header">
|
<header style="margin-top: 10px;" class="post-header">
|
||||||
<h1 class="thread-subject">{ thread.Subject } -</h1>
|
<h1 class="thread-subject">{ thread.Subject } -</h1>
|
||||||
<span class="post-author">{ post.Author }</span>
|
<span class="post-author">{ post.Author }</span>
|
||||||
@@ -61,25 +62,26 @@ templ PostReply(post database.Post) {
|
|||||||
<span class="post-replies"></span>
|
<span class="post-replies"></span>
|
||||||
</header>
|
</header>
|
||||||
if post.MediaPath != "" {
|
if post.MediaPath != "" {
|
||||||
{{ imgInfo := util.GetPostImageInfo(post.MediaPath) }}
|
|
||||||
<div>
|
<div>
|
||||||
<div style="margin-bottom: 2px;">
|
<div style="margin-bottom: 2px;">
|
||||||
File:
|
File:
|
||||||
<a href={ templ.URL("/static/img/posts/full/" + post.MediaPath) } class="post-filename">
|
<a href={ templ.URL("/static/media/posts/full/" + post.MediaPath) } class="post-filename">
|
||||||
{ post.MediaPath }
|
{ post.MediaPath }
|
||||||
</a>
|
</a>
|
||||||
<div class="post-img-info">
|
<div class="post-img-info">
|
||||||
(<span>{ util.FormatBytes(imgInfo.Size) }</span>,
|
<span>{ util.FormatFileInfo(util.GetPostFileInfo(post.MediaPath)) }</span>
|
||||||
<span>{ strconv.Itoa(imgInfo.Width) }</span>x<span>{ strconv.Itoa(imgInfo.Height) }</span>)
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<img
|
<img
|
||||||
onclick="togglePostImage(this)"
|
onclick="togglePostFile(this)"
|
||||||
loading="lazy"
|
loading="lazy"
|
||||||
src={ fmt.Sprintf("/static/img/posts/thumb/%s",
|
src={ fmt.Sprintf("/static/media/posts/thumb/%s",
|
||||||
post.MediaPath) }
|
post.ThumbPath) }
|
||||||
|
data-full={ post.MediaPath }
|
||||||
|
data-thumb={ post.ThumbPath }
|
||||||
class="post-img"
|
class="post-img"
|
||||||
/>
|
/>
|
||||||
|
<video controls style="display: none;" class="post-vid"></video>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
<p class="post-body">
|
<p class="post-body">
|
||||||
@@ -128,7 +130,14 @@ templ Thread(board database.Board, thread database.Thread, posts []database.Post
|
|||||||
</tr>
|
</tr>
|
||||||
<tr class="new-post-form-field">
|
<tr class="new-post-form-field">
|
||||||
<th>File</th>
|
<th>File</th>
|
||||||
<td><input id="newPostFile" name="file" type="file"/></td>
|
<td>
|
||||||
|
<input
|
||||||
|
accept="image/*,video/mp4,video/webm,video/ogg"
|
||||||
|
id="newPostFile"
|
||||||
|
name="file"
|
||||||
|
type="file"
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|||||||
Reference in New Issue
Block a user