Implement attachments for article

This commit is contained in:
ChronosX88 2022-04-12 03:29:22 +03:00
parent 537c3abe82
commit 56205ffdd1
Signed by: ChronosXYZ
GPG Key ID: 085A69A82C8C511A
8 changed files with 72 additions and 15 deletions

1
.gitignore vendored
View File

@ -1,3 +1,4 @@
*.db *.db
.config.toml .config.toml
.idea .idea
.upload

View File

@ -9,9 +9,9 @@
- :heavy_check_mark: Wildmat support - :heavy_check_mark: Wildmat support
- :heavy_check_mark: Database (SQLite) - :heavy_check_mark: Database (SQLite)
- :heavy_check_mark: Basic article posting - :heavy_check_mark: Basic article posting
- :construction: Article retrieving - :heavy_check_mark: Article retrieving
- :construction: Multipart article support - :heavy_check_mark: Multipart article support
- :x: Transit mode - :construction: Transit mode
- :x: Authentication - :x: Authentication
#### Commands #### Commands
@ -21,8 +21,8 @@
- :heavy_check_mark: `CAPABILITIES` - :heavy_check_mark: `CAPABILITIES`
- :heavy_check_mark: `QUIT` - :heavy_check_mark: `QUIT`
- :construction: Article posting - :construction: Article posting
- :construction: `POST` - :heavy_check_mark: `POST`
- :x: `IHAVE` - :construction: `IHAVE`
- :heavy_check_mark: Article retrieving - :heavy_check_mark: Article retrieving
- :heavy_check_mark: `ARTICLE` - :heavy_check_mark: `ARTICLE`
- :heavy_check_mark: `HEAD` - :heavy_check_mark: `HEAD`

View File

@ -0,0 +1,11 @@
-- +goose Up
CREATE TABLE IF NOT EXISTS attachments_articles_mapping (
article_id INTEGER REFERENCES articles(id),
content_type TEXT NOT NULL,
attachment_id TEXT NOT NULL
);
-- +goose Down
DROP TABLE IF EXISTS attachments_articles_mapping;

View File

@ -124,6 +124,15 @@ func (sb *SQLiteBackend) SaveArticle(a models.Article, groups []string) error {
return err return err
} }
} }
// save attachments into db
for _, v := range a.Attachments {
_, err = sb.db.Exec("INSERT INTO attachments_articles_mapping (article_id, content_type, attachment_id) VALUES (?, ?, ?)", articleID, v.ContentType, v.FileName)
if err != nil {
return err
}
}
return err return err
} }
@ -135,6 +144,9 @@ func (sb *SQLiteBackend) GetArticle(messageID string) (models.Article, error) {
if err := sb.db.Get(&a.ArticleNumber, "SELECT article_number FROM articles_to_groups WHERE article_id = ?", a.ID); err != nil { if err := sb.db.Get(&a.ArticleNumber, "SELECT article_number FROM articles_to_groups WHERE article_id = ?", a.ID); err != nil {
return a, err return a, err
} }
if err := sb.db.Select(&a.Attachments, "SELECT content_type, attachment_id FROM attachments_articles_mapping WHERE article_id = ?", a.ID); err != nil {
return a, err
}
return a, json.Unmarshal([]byte(a.HeaderRaw), &a.Header) return a, json.Unmarshal([]byte(a.HeaderRaw), &a.Header)
} }
@ -144,6 +156,9 @@ func (sb *SQLiteBackend) GetArticleByNumber(g *models.Group, num int) (models.Ar
return a, err return a, err
} }
a.ArticleNumber = num a.ArticleNumber = num
if err := sb.db.Select(&a.Attachments, "SELECT content_type, attachment_id FROM attachments_articles_mapping WHERE article_id = ?", a.ID); err != nil {
return a, err
}
return a, json.Unmarshal([]byte(a.HeaderRaw), &a.Header) return a, json.Unmarshal([]byte(a.HeaderRaw), &a.Header)
} }

View File

@ -15,6 +15,7 @@ type Config struct {
BackendType string `toml:"backend_type"` BackendType string `toml:"backend_type"`
Domain string `toml:"domain"` Domain string `toml:"domain"`
SQLite SQLiteBackendConfig `toml:"sqlite"` SQLite SQLiteBackendConfig `toml:"sqlite"`
UploadPath string `toml:"upload_path"`
} }
type SQLiteBackendConfig struct { type SQLiteBackendConfig struct {

View File

@ -17,4 +17,10 @@ type Article struct {
Header textproto.MIMEHeader `db:"-"` Header textproto.MIMEHeader `db:"-"`
Envelope *enmime.Envelope `db:"-"` Envelope *enmime.Envelope `db:"-"`
ArticleNumber int `db:"-"` ArticleNumber int `db:"-"`
Attachments []Attachment
}
type Attachment struct {
ContentType string `db:"content_type"`
FileName string `db:"attachment_id"`
} }

View File

@ -12,7 +12,9 @@ import (
"github.com/ChronosX88/yans/internal/utils" "github.com/ChronosX88/yans/internal/utils"
"github.com/google/uuid" "github.com/google/uuid"
"github.com/jhillyerd/enmime" "github.com/jhillyerd/enmime"
"io/ioutil"
"net/mail" "net/mail"
"path"
"strconv" "strconv"
"strings" "strings"
"time" "time"
@ -22,9 +24,10 @@ type Handler struct {
handlers map[string]func(s *Session, command string, arguments []string, id uint) error handlers map[string]func(s *Session, command string, arguments []string, id uint) error
backend backend.StorageBackend backend backend.StorageBackend
serverDomain string serverDomain string
uploadPath string
} }
func NewHandler(b backend.StorageBackend, serverDomain string) *Handler { func NewHandler(b backend.StorageBackend, serverDomain, uploadPath string) *Handler {
h := &Handler{} h := &Handler{}
h.backend = b h.backend = b
h.handlers = map[string]func(s *Session, command string, arguments []string, id uint) error{ h.handlers = map[string]func(s *Session, command string, arguments []string, id uint) error{
@ -49,6 +52,7 @@ func NewHandler(b backend.StorageBackend, serverDomain string) *Handler {
protocol.CommandXover: h.handleOver, protocol.CommandXover: h.handleOver,
} }
h.serverDomain = serverDomain h.serverDomain = serverDomain
h.uploadPath = uploadPath
return h return h
} }
@ -347,13 +351,29 @@ func (h *Handler) handlePost(s *Session, command string, arguments []string, id
return err return err
} }
} }
if !parentMessage.Thread.Valid {
var parentHeader mail.Header var parentHeader mail.Header
err = json.Unmarshal([]byte(parentMessage.HeaderRaw), &parentHeader) err = json.Unmarshal([]byte(parentMessage.HeaderRaw), &parentHeader)
parentMessageID := parentHeader.Get("Message-ID") parentMessageID := parentHeader.Get("Message-ID")
a.Thread = sql.NullString{String: parentMessageID, Valid: true} a.Thread = sql.NullString{String: parentMessageID, Valid: true}
} else { }
a.Thread = parentMessage.Thread
if len(envelope.Attachments) > 0 {
// save attachments
for _, v := range envelope.Attachments {
if v.ContentType != "image/jpeg" && v.ContentType != "image/png" && v.ContentType != "image/gif" {
return s.tconn.PrintfLine(protocol.NNTPResponse{Code: 441, Message: "disallowed attachment type"}.String())
}
ext_ := strings.Split(v.FileName, ".")
ext := ext_[len(ext_)-1]
fileName := uuid.New().String() + "." + ext
err = ioutil.WriteFile(path.Join(h.uploadPath, fileName), v.Content, 0644)
if err != nil {
return err
}
a.Attachments = append(a.Attachments, models.Attachment{
ContentType: v.ContentType,
FileName: fileName,
})
} }
} }
@ -493,7 +513,10 @@ func (h *Handler) handleArticle(s *Session, command string, arguments []string,
builder = builder.Header(k, j) builder = builder.Header(k, j)
} }
} }
builder = builder.Text([]byte(a.Body)) // FIXME currently only plain text is supported builder = builder.Text([]byte(a.Body))
for _, v := range a.Attachments {
builder = builder.AddFileAttachment(path.Join(h.uploadPath, v.FileName))
}
p, err := builder.Build() p, err := builder.Build()
if err != nil { if err != nil {
return err return err

View File

@ -97,7 +97,7 @@ func (ns *NNTPServer) Start() error {
id, _ := uuid.NewUUID() id, _ := uuid.NewUUID()
closed := make(chan bool) closed := make(chan bool)
session, err := NewSession(ctx, conn, Capabilities, id.String(), closed, NewHandler(ns.backend, ns.cfg.Domain)) session, err := NewSession(ctx, conn, Capabilities, id.String(), closed, NewHandler(ns.backend, ns.cfg.Domain, ns.cfg.UploadPath))
ns.sessionPoolMutex.Lock() ns.sessionPoolMutex.Lock()
ns.sessionPool[id.String()] = session ns.sessionPool[id.String()] = session
ns.sessionPoolMutex.Unlock() ns.sessionPoolMutex.Unlock()