Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions pkg/bot/bot.go
Original file line number Diff line number Diff line change
Expand Up @@ -91,4 +91,9 @@ func (b *Bot) setupHandlers() error {
func (b *Bot) startJobs() {
b.container.ChannelDeleteJob.Run()
b.container.EmojiUpdateInfoJob.Run()

// データベースバックアップJob(設定で有効化されている場合のみ)
if err := b.container.DatabaseBackupJob.Run(); err != nil {
fmt.Printf("Database backup failed: %v\n", err)
}
}
37 changes: 36 additions & 1 deletion pkg/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,16 @@ type Config struct {
SavePath string
DatabasePath string
IsDebug bool

// S3 Configuration
AWSAccessKeyID string
AWSSecretAccessKey string
AWSRegion string
S3Bucket string
S3Endpoint string
S3ForcePathStyle bool
UseS3 bool
EnableDatabaseBackup bool
}

func LoadConfig() (*Config, error) {
Expand All @@ -30,11 +40,26 @@ func LoadConfig() (*Config, error) {
return nil, errors.Config("failed to load settings.env", err)
}

isDebug, err := strconv.ParseBool(os.Getenv("is_debug"))
isDebug, err := strconv.ParseBool(os.Getenv("debug"))
if err != nil {
isDebug = false
}

useS3, err := strconv.ParseBool(os.Getenv("use_s3"))
if err != nil {
useS3 = false
}

s3ForcePathStyle, err := strconv.ParseBool(os.Getenv("s3_force_path_style"))
if err != nil {
s3ForcePathStyle = false
}

enableDatabaseBackup, err := strconv.ParseBool(os.Getenv("enable_database_backup"))
if err != nil {
enableDatabaseBackup = false
}

config := &Config{
GuildID: strings.TrimSpace(os.Getenv("guild_id")),
BotToken: strings.TrimSpace(os.Getenv("bot_token")),
Expand All @@ -47,6 +72,16 @@ func LoadConfig() (*Config, error) {
SavePath: strings.TrimSpace(os.Getenv("save_path")),
DatabasePath: strings.TrimSpace(os.Getenv("database_path")),
IsDebug: isDebug,

// S3 Configuration
AWSAccessKeyID: strings.TrimSpace(os.Getenv("aws_access_key_id")),
AWSSecretAccessKey: strings.TrimSpace(os.Getenv("aws_secret_access_key")),
AWSRegion: strings.TrimSpace(os.Getenv("aws_region")),
S3Bucket: strings.TrimSpace(os.Getenv("s3_bucket")),
S3Endpoint: strings.TrimSpace(os.Getenv("s3_endpoint")),
S3ForcePathStyle: s3ForcePathStyle,
UseS3: useS3,
EnableDatabaseBackup: enableDatabaseBackup,
}

// Set default values
Expand Down
10 changes: 8 additions & 2 deletions pkg/container/container.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,9 @@ type Container struct {
ComponentHandler handler.ComponentHandler

// Jobs
ChannelDeleteJob job.Job
EmojiUpdateInfoJob job.Job
ChannelDeleteJob job.Job
EmojiUpdateInfoJob job.Job
DatabaseBackupJob job.DatabaseBackupJob

// Discord Session
Session *discordgo.Session
Expand Down Expand Up @@ -85,6 +86,10 @@ func NewContainer(cfg *config.Config) (*Container, error) {
// Initialize jobs
channelDeleteJob := job.NewChannelDeleteJob(emojiRepo, discordRepo)
emojiUpdateInfoJob := job.NewEmojiUpdateInfoJob(emojiRepo, misskeyRepo)
databaseBackupJob, err := job.NewDatabaseBackupJob(cfg)
if err != nil {
return nil, errors.Config("failed to initialize database backup job", err)
}

container := &Container{
Config: cfg,
Expand All @@ -98,6 +103,7 @@ func NewContainer(cfg *config.Config) (*Container, error) {
ComponentHandler: componentHandler,
ChannelDeleteJob: channelDeleteJob,
EmojiUpdateInfoJob: emojiUpdateInfoJob,
DatabaseBackupJob: databaseBackupJob,
Session: session,
Version: string(version),
}
Expand Down
82 changes: 62 additions & 20 deletions pkg/handler/processor/upload_handler.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package processor

import (
"bytes"
"os"
"path/filepath"

Expand All @@ -9,15 +10,21 @@ import (
"MisskeyEmojiBot/pkg/config"
"MisskeyEmojiBot/pkg/entity"
"MisskeyEmojiBot/pkg/handler"
"MisskeyEmojiBot/pkg/repository"
"MisskeyEmojiBot/pkg/utility"
)

type uploadHandler struct {
config config.Config
s3Repo repository.S3Repository
}

func NewUploadHandler(cfg config.Config) handler.EmojiProcessHandler {
return &uploadHandler{config: cfg}
var s3Repo repository.S3Repository
if cfg.UseS3 {
s3Repo, _ = repository.NewS3Repository(&cfg)
}
return &uploadHandler{config: cfg, s3Repo: s3Repo}
}

func (h *uploadHandler) Request(emoji *entity.Emoji, s *discordgo.Session, cID string) (entity.Response, error) {
Expand All @@ -42,27 +49,62 @@ func (h *uploadHandler) Response(emoji *entity.Emoji, s *discordgo.Session, m *d
"対応ファイルは`.png`,`.jpg`,`.jpeg`,`.gif`です。")
return response, nil
}
emoji.FilePath = filepath.Join(h.config.SavePath, emoji.ID+ext)
err := utility.EmojiDownload(attachment.URL, emoji.FilePath)
if err != nil {
_, _ = s.ChannelMessageSend(m.ChannelID, ": Error! \n"+
"申請中にエラーが発生しました。URLを確認して再アップロードを行うか、管理者へ問い合わせを行ってください。#01a")
return response, nil
}
if h.config.UseS3 && h.s3Repo != nil {
fileData, err := utility.EmojiDownloadToBytes(attachment.URL)
if err != nil {
_, _ = s.ChannelMessageSend(m.ChannelID, ": Error! \n"+
"申請中にエラーが発生しました。URLを確認して再アップロードを行うか、管理者へ問い合わせを行ってください。#01a")
return response, nil
}

file, err := os.Open(emoji.FilePath)
if err != nil {
_, _ = s.ChannelMessageSend(m.ChannelID, ": Error! \n"+
"申請中にエラーが発生しました。管理者へ問い合わせを行ってください。#01b")
return response, nil
}
defer func() { _ = file.Close() }()
key := "emojis/" + emoji.ID + ext
contentType := repository.GetContentTypeFromExtension(attachment.Filename)
fileURL, err := h.s3Repo.UploadFile(key, fileData, contentType)
if err != nil {
_, _ = s.ChannelMessageSend(m.ChannelID, ": Error! \n"+
"申請中にエラーが発生しました。S3へのアップロードに失敗しました。#01sa")
return response, nil
}

_, err = s.ChannelFileSend(m.ChannelID, emoji.FilePath, file)
if err != nil {
_, _ = s.ChannelMessageSend(m.ChannelID, ": Error! \n"+
"申請中にエラーが発生しました。管理者へ問い合わせを行ってください。#01d")
return response, nil
emoji.FilePath = fileURL

_, err = s.ChannelMessageSendComplex(m.ChannelID, &discordgo.MessageSend{
Content: "アップロード完了: " + fileURL,
Files: []*discordgo.File{
{
Name: attachment.Filename,
Reader: bytes.NewReader(fileData),
},
},
})
if err != nil {
_, _ = s.ChannelMessageSend(m.ChannelID, ": Error! \n"+
"申請中にエラーが発生しました。管理者へ問い合わせを行ってください。#01sb")
return response, nil
}
} else {
emoji.FilePath = filepath.Join(h.config.SavePath, emoji.ID+ext)
err := utility.EmojiDownload(attachment.URL, emoji.FilePath)
if err != nil {
_, _ = s.ChannelMessageSend(m.ChannelID, ": Error! \n"+
"申請中にエラーが発生しました。URLを確認して再アップロードを行うか、管理者へ問い合わせを行ってください。#01a")
return response, nil
}

file, err := os.Open(emoji.FilePath)
if err != nil {
_, _ = s.ChannelMessageSend(m.ChannelID, ": Error! \n"+
"申請中にエラーが発生しました。管理者へ問い合わせを行ってください。#01b")
return response, nil
}
defer func() { _ = file.Close() }()

_, err = s.ChannelFileSend(m.ChannelID, emoji.FilePath, file)
if err != nil {
_, _ = s.ChannelMessageSend(m.ChannelID, ": Error! \n"+
"申請中にエラーが発生しました。管理者へ問い合わせを行ってください。#01d")
return response, nil
}
}

response.IsSuccess = true
Expand Down
128 changes: 128 additions & 0 deletions pkg/job/database_backup_job.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
package job

import (
"fmt"
"io"
"os"
"time"

"MisskeyEmojiBot/pkg/config"
"MisskeyEmojiBot/pkg/repository"
)

type DatabaseBackupJob interface {
Run() error
}

type databaseBackupJob struct {
config *config.Config
s3Repo repository.S3Repository
}

func NewDatabaseBackupJob(cfg *config.Config) (DatabaseBackupJob, error) {
job := &databaseBackupJob{config: cfg}

if cfg.UseS3 {
s3Repo, err := repository.NewS3Repository(cfg)
if err != nil {
return nil, fmt.Errorf("failed to create S3 repository: %w", err)
}
job.s3Repo = s3Repo
}

return job, nil
}

func (j *databaseBackupJob) Run() error {

if !j.config.EnableDatabaseBackup {
fmt.Println("Database backup job is disabled in configuration.")
return nil
}

if j.config.DatabasePath == "" {
return fmt.Errorf("database path is not configured")
}

cleanRequest := time.NewTicker(12 * time.Hour)
go func() {
for range cleanRequest.C {
if !j.config.UseS3 || j.s3Repo == nil {
// S3が有効でない場合はローカルバックアップのみ
j.createLocalBackup()
}

// データベースファイルを読み込む
dbData, err := j.readDatabaseFile()
if err != nil {
fmt.Errorf("failed to read database file: %w", err)
}

// S3にバックアップをアップロード
timestamp := time.Now().Format("2006-01-02_15-04-05")
backupKey := fmt.Sprintf("backups/database_%s.db", timestamp)

_, err = j.s3Repo.UploadFile(backupKey, dbData, "application/octet-stream")
if err != nil {
fmt.Errorf("failed to upload backup to S3: %w", err)
}

fmt.Printf("Database backup uploaded to S3: %s\n", backupKey)

// ローカルバックアップも作成(オプション)
if err := j.createLocalBackup(); err != nil {
fmt.Printf("Warning: Local backup failed: %v\n", err)
}
}
}()

return nil
}

func (j *databaseBackupJob) readDatabaseFile() ([]byte, error) {
file, err := os.Open(j.config.DatabasePath)
if err != nil {
return nil, err
}
defer file.Close()

data, err := io.ReadAll(file)
if err != nil {
return nil, err
}

return data, nil
}

func (j *databaseBackupJob) createLocalBackup() error {
timestamp := time.Now().Format("2006-01-02_15-04-05")
backupPath := fmt.Sprintf("%sbackup_%s.db", j.config.SavePath, timestamp)

// 元のファイルを開く
src, err := os.Open(j.config.DatabasePath)
if err != nil {
return err
}
defer src.Close()

// バックアップディレクトリを作成
if err := os.MkdirAll(j.config.SavePath, os.ModePerm); err != nil {
return err
}

// バックアップファイルを作成
dst, err := os.Create(backupPath)
if err != nil {
return err
}
defer dst.Close()

// ファイルをコピー
_, err = io.Copy(dst, src)
if err != nil {
return err
}

fmt.Printf("Local database backup created: %s\n", backupPath)
return nil
}
Loading