From f7485e035ee52ac949014a2cd13af6bc97983393 Mon Sep 17 00:00:00 2001 From: rxdn <29165304+rxdn@users.noreply.github.com> Date: Wed, 3 Jul 2024 17:25:51 +0100 Subject: [PATCH] Update minio client and check in utility scripts --- cmd/bucketmanager/lifecycleconfiguration.go | 30 ---- cmd/bucketmanager/main.go | 40 ----- cmd/cleanuser/main.go | 186 ++++++++++++++------ cmd/deletetranscript/main.go | 78 ++++++++ cmd/exportguild/main.go | 64 +++++-- cmd/exportuser/main.go | 108 +++++++++--- cmd/fix-file-names/main.go | 88 +++++++++ cmd/logarchiver/main.go | 7 +- cmd/purgemodmail/main.go | 32 ---- go.mod | 27 ++- go.sum | 34 +++- pkg/http/purgeguild.go | 18 +- pkg/http/s3.go | 67 ------- pkg/http/server.go | 9 +- pkg/http/ticketget.go | 6 +- pkg/http/ticketupload.go | 2 +- pkg/s3client/client.go | 81 +++++++++ pkg/s3client/errors.go | 5 + 18 files changed, 594 insertions(+), 288 deletions(-) delete mode 100644 cmd/bucketmanager/lifecycleconfiguration.go delete mode 100644 cmd/bucketmanager/main.go create mode 100644 cmd/deletetranscript/main.go create mode 100644 cmd/fix-file-names/main.go delete mode 100644 cmd/purgemodmail/main.go delete mode 100644 pkg/http/s3.go create mode 100644 pkg/s3client/client.go create mode 100644 pkg/s3client/errors.go diff --git a/cmd/bucketmanager/lifecycleconfiguration.go b/cmd/bucketmanager/lifecycleconfiguration.go deleted file mode 100644 index 82fe2b1..0000000 --- a/cmd/bucketmanager/lifecycleconfiguration.go +++ /dev/null @@ -1,30 +0,0 @@ -package main - -import "encoding/xml" - -type ( - LifecycleConfiguration struct { - XMLName xml.Name `xml:"LifecycleConfiguration"` - Xmlns string `xml:"xmlns,attr"` - Id string `xml:"Rule>ID"` - Prefix string `xml:"Rule>Prefix"` - Status string `xml:"Rule>Status"` - Days int `xml:"Rule>Expiration>Days"` - } -) - -func NewLifecycleConfiguration(id, prefix string, status bool, days int) LifecycleConfiguration { - lc := LifecycleConfiguration{ - Xmlns: "http://s3.amazonaws.com/doc/2006-03-01/", - Id: id, - Prefix: prefix, - Status: "Disabled", - Days: days, - } - - if status { - lc.Status = "Enabled" - } - - return lc -} diff --git a/cmd/bucketmanager/main.go b/cmd/bucketmanager/main.go deleted file mode 100644 index 509a630..0000000 --- a/cmd/bucketmanager/main.go +++ /dev/null @@ -1,40 +0,0 @@ -package main - -import ( - "bytes" - "encoding/xml" - "flag" - "github.com/minio/minio-go/v6" -) - -var ( - endpoint = flag.String("endpoint", "nyc3.digitaloceanspaces.com", "the S3 compatible object storage provider endpoint") - accessKey = flag.String("accesskey", "", "access key ID") - secretKey = flag.String("secretkey", "", "secret key") - bucket = flag.String("bucket", "", "the name of the bucket to manage") - - rule = flag.String("rule", "", "A unique string identifying the rule. It may contain up to 255 characters including spaces") - prefix = flag.String("prefix", "", "Prefix of files to apply the rule to") - status = flag.Bool("enabled", false, "Whether or not the rule will be enabled") - days = flag.Int("expiration.days", 90, "An integer specifying the number of days after an object's creation until the rule takes effect") -) - -func main() { - flag.Parse() - - client, err := minio.New(*endpoint, *accessKey, *secretKey, false) - if err != nil { - panic(err) - } - - var buff bytes.Buffer - encoder := xml.NewEncoder(&buff) - encoder.Indent(" ", " ") - if err := encoder.Encode(NewLifecycleConfiguration(*rule, *prefix, *status, *days)); err != nil { - panic(err) - } - - if err := client.SetBucketLifecycle(*bucket, string(buff.Bytes())); err != nil { - panic(err) - } -} diff --git a/cmd/cleanuser/main.go b/cmd/cleanuser/main.go index 8dcd1d5..f7c4bd6 100644 --- a/cmd/cleanuser/main.go +++ b/cmd/cleanuser/main.go @@ -1,100 +1,121 @@ package main import ( + "context" "encoding/json" + "errors" "flag" "fmt" "github.com/TicketsBot/common/encryption" "github.com/TicketsBot/logarchiver/pkg/config" - "github.com/TicketsBot/logarchiver/pkg/http" "github.com/TicketsBot/logarchiver/pkg/model" "github.com/TicketsBot/logarchiver/pkg/model/v1" - v22 "github.com/TicketsBot/logarchiver/pkg/model/v2" - "github.com/minio/minio-go/v6" + "github.com/TicketsBot/logarchiver/pkg/model/v2" + "github.com/TicketsBot/logarchiver/pkg/s3client" + "github.com/minio/minio-go/v7" + "github.com/minio/minio-go/v7/pkg/credentials" "github.com/rxdn/gdl/objects/channel/message" - "go.uber.org/zap" "os" "strconv" "strings" ) var ( - endpoint, accessKey, secretKey, bucket string - userId = flag.Uint64("userid", 0, "user ID to purge") guildId = flag.Uint64("guildid", 0, "guild ID the ticket is from") - ticketId = flag.Int("ticket", 0, "ticket ID to clean") + ticketIds = flag.String("ticket", "", "ticket ID(s) to clean") all = flag.Bool("all", false, "apply to all tickets") - encryptionKey = flag.String("key", "", "encryption key") // to any keen eyes looking at commit history: the key has been ommitted and all transcripts have been re-encrypted + encryptionKey = flag.String("key", "", "encryption key") + csv = flag.String("csv", "", "csv file to read from") ) -func loadEnvvars() { - endpoint = strip(os.Getenv("S3_ENDPOINT")) - accessKey = strip(os.Getenv("S3_ACCESS")) - secretKey = strip(os.Getenv("S3_SECRET")) - bucket = strip(os.Getenv("S3_BUCKET")) -} - -func strip(s string) string { - return strings.ReplaceAll(strings.ReplaceAll(s, "\r", ""), "\n", "") -} - func main() { flag.Parse() - loadEnvvars() + cfg := config.Parse() - if *ticketId == 0 && !*all || *ticketId > 1 && *all { - panic("ticket or all must be set and are mutually exclusive") + // ensure only one is set + if *ticketIds == "" && !*all && *csv == "" { + panic("either -ticket, -all or -csv must be set") } - conf := config.Config{ - Endpoint: endpoint, - Bucket: bucket, - AccessKey: accessKey, - SecretKey: secretKey, - } + client, err := minio.New(cfg.Endpoint, &minio.Options{ + Creds: credentials.NewStaticV4(cfg.AccessKey, cfg.SecretKey, ""), + }) - client, err := minio.New(endpoint, accessKey, secretKey, false) if err != nil { panic(err) } - logger, err := zap.NewDevelopment() - if err != nil { - panic(err) - } - - server := http.NewServer(logger, conf, client) + s3Client := s3client.NewS3Client(client, cfg.Bucket) var count int - if !*all { - count = clean(server, *ticketId) - } else { - done := make(chan struct{}) - defer close(done) - - prefix := fmt.Sprintf("%d/", *guildId) - for obj := range client.ListObjectsV2WithMetadata(bucket, prefix, true, done) { - suffix := strings.TrimPrefix(obj.Key, prefix) - suffix = strings.TrimPrefix(suffix, "free-") - ticketId, err := strconv.Atoi(suffix) + if *csv != "" { + tickets := parseCsv(*csv) + for guildId, ids := range tickets { + for _, ticketId := range ids { + msgCount, err := clean(s3Client, guildId, ticketId) + if err != nil { + if errors.Is(err, s3client.ErrTicketNotFound) { + fmt.Printf("ticket %d/%d not found\n", guildId, ticketId) + continue + } else { + panic(err) + } + } + + count += msgCount + + fmt.Printf("cleaned %d/%d (%d msgs)\n", guildId, ticketId, msgCount) + } + } + } else if *all { + keys, err := s3Client.GetAllKeysForGuild(context.Background(), *guildId) + if err != nil { + panic(err) + } + + for _, key := range keys { + ticketId, err := strconv.Atoi(key[strings.LastIndex(key, "/")+1:]) if err != nil { - fmt.Printf("error occurred while parsing id of %s: %v\n", obj.Key, err) + fmt.Printf("error occurred while parsing id of %s: %v\n", key, err) continue } - count += clean(server, ticketId) + msgCount, err := clean(s3Client, *guildId, ticketId) + if err != nil { + panic(err) + } + + count += msgCount + fmt.Printf("cleaned %d\n", ticketId) } + } else { + split := strings.Split(*ticketIds, ",") + for _, raw := range split { + ticketId, err := strconv.Atoi(raw) + if err != nil { + panic(err) + } + + msgCount, err := clean(s3Client, *guildId, ticketId) + if err != nil { + panic(err) + } + + count += msgCount + + fmt.Printf("cleaned ticket %d\n", ticketId) + } } fmt.Printf("Cleaned %d messages\n", count) } -func clean(server *http.Server, ticketId int) (count int) { - data, err := server.GetTicket(bucket, *guildId, ticketId) +func clean(client *s3client.S3Client, guildId uint64, ticketId int) (int, error) { + data, err := client.GetTicket(context.Background(), guildId, ticketId) if err != nil { - panic(err) + return 0, err } data, err = encryption.Decompress(data) @@ -107,7 +128,7 @@ func clean(server *http.Server, ticketId int) (count int) { panic(err) } - var transcript v22.Transcript + var transcript v2.Transcript version := model.GetVersion(data) switch version { @@ -126,7 +147,7 @@ func clean(server *http.Server, ticketId int) (count int) { panic(fmt.Sprintf("Unknown version %d", version)) } - transcript.Entities.Users[*userId] = v22.User{ + transcript.Entities.Users[*userId] = v2.User{ Id: 0, Username: "Removed for privacy", Discriminator: 0, @@ -134,6 +155,7 @@ func clean(server *http.Server, ticketId int) (count int) { Bot: false, } + var count int for i, message := range transcript.Messages { if message.AuthorId == *userId { count++ @@ -159,10 +181,64 @@ func clean(server *http.Server, ticketId int) (count int) { data = encryption.Compress(data) - err = server.UploadTicket(bucket, *guildId, ticketId, data) + err = client.StoreTicket(context.Background(), guildId, ticketId, data) + if err != nil { + panic(err) + } + + return count, nil +} + +func parseCsv(file string) map[uint64][]int { + bytes, err := os.ReadFile(file) if err != nil { panic(err) } - return + lines := strings.Split(string(bytes), "\n") + + header := strings.Split(lines[0], ",") + + guildIdIdx := -1 + ticketIdIdx := -1 + + for i, h := range header { + if h == "guild_id" { + guildIdIdx = i + } else if h == "ticket_id" { + ticketIdIdx = i + } + } + + if guildIdIdx == -1 || ticketIdIdx == -1 { + panic("Invalid CSV format") + } + + mapping := make(map[uint64][]int) + + for _, line := range lines[1:] { + if line == "" { + continue + } + + values := strings.Split(line, ",") + + guildId, err := strconv.ParseUint(values[guildIdIdx], 10, 64) + if err != nil { + panic(err) + } + + ticketId, err := strconv.Atoi(values[ticketIdIdx]) + if err != nil { + panic(err) + } + + if _, ok := mapping[guildId]; !ok { + mapping[guildId] = make([]int, 0) + } + + mapping[guildId] = append(mapping[guildId], ticketId) + } + + return mapping } diff --git a/cmd/deletetranscript/main.go b/cmd/deletetranscript/main.go new file mode 100644 index 0000000..0067ec6 --- /dev/null +++ b/cmd/deletetranscript/main.go @@ -0,0 +1,78 @@ +package main + +import ( + "context" + "flag" + "fmt" + "github.com/TicketsBot/logarchiver/pkg/config" + "github.com/TicketsBot/logarchiver/pkg/s3client" + "github.com/minio/minio-go/v7" + "github.com/minio/minio-go/v7/pkg/credentials" + "strconv" + "strings" +) + +var ( + guildId = flag.Uint64("guildid", 0, "guild ID the ticket is from") + ticketId = flag.String("ticket", "", "ticket ID(s) to clean") + all = flag.Bool("all", false, "apply to all tickets") +) + +func main() { + flag.Parse() + cfg := config.Parse() + + if guildId == nil || *guildId == 0 { + panic("guild id must be set") + } + + m, err := minio.New(cfg.Endpoint, &minio.Options{ + Creds: credentials.NewStaticV4(cfg.AccessKey, cfg.SecretKey, ""), + }) + + if err != nil { + panic(err) + } + + client := s3client.NewS3Client(m, cfg.Bucket) + + if *all { + keys, err := client.GetAllKeysForGuild(context.Background(), *guildId) + if err != nil { + panic(err) + } + + for _, key := range keys { + ticketId, err := strconv.Atoi(key[strings.LastIndex(key, "/")+1:]) + if err != nil { + fmt.Printf("error occurred while parsing id of %s: %v\n", key, err) + continue + } + + must(client.DeleteTicket(context.Background(), *guildId, ticketId)) + + fmt.Printf("cleaned %d\n", ticketId) + } + } else if *ticketId != "" { + split := strings.Split(*ticketId, ",") + + for _, id := range split { + parsed, err := strconv.Atoi(id) + if err != nil { + fmt.Printf("error occurred while parsing id of %s: %v\n", id, err) + continue + } + + must(client.DeleteTicket(context.Background(), *guildId, parsed)) + fmt.Printf("deleted %d\n", parsed) + } + } else { + panic("all or ticket flag must be set") + } +} + +func must(err error) { + if err != nil { + panic(err) + } +} diff --git a/cmd/exportguild/main.go b/cmd/exportguild/main.go index 4ffee08..193d456 100644 --- a/cmd/exportguild/main.go +++ b/cmd/exportguild/main.go @@ -2,28 +2,34 @@ package main import ( "bytes" + "context" "encoding/json" "flag" "fmt" "github.com/TicketsBot/common/encryption" "github.com/TicketsBot/logarchiver/pkg/config" - "github.com/TicketsBot/logarchiver/pkg/http" "github.com/TicketsBot/logarchiver/pkg/model" "github.com/TicketsBot/logarchiver/pkg/model/v1" v22 "github.com/TicketsBot/logarchiver/pkg/model/v2" - "github.com/minio/minio-go/v6" + "github.com/TicketsBot/logarchiver/pkg/s3client" + "github.com/minio/minio-go/v7" + "github.com/minio/minio-go/v7/pkg/credentials" "github.com/rxdn/gdl/objects/channel/message" + "golang.org/x/sync/errgroup" "os" "strconv" "strings" ) +const workers = 15 + var ( guildId = flag.Uint64("guildid", 0, "guild id to export") key = flag.String("key", "", "aes key") ticketId = flag.Int("ticketid", 0, "set to export a single ticket") convert = flag.Bool("convert", false, "convert to v2 if necessary") userWhitelist = flag.Uint64("userwhitelist", 0, "only export tickets from this user") + after = flag.Int("after", 0, "export ticket IDs above this value (inclusive)") ) func main() { @@ -31,38 +37,62 @@ func main() { conf := config.Parse() // create minio client - client, err := minio.New(conf.Endpoint, conf.AccessKey, conf.SecretKey, false) + m, err := minio.New(conf.Endpoint, &minio.Options{ + Creds: credentials.NewStaticV4(conf.AccessKey, conf.SecretKey, ""), + }) if err != nil { panic(err) } - s := http.NewServer(conf, client) + client := s3client.NewS3Client(m, conf.Bucket) // likely to be file exists _ = os.Mkdir(fmt.Sprintf("export/%d", *guildId), 0) if ticketId != nil && *ticketId > 0 { - export(*ticketId, s) + export(*ticketId, client) } else { - doneCh := make(chan struct{}) - defer close(doneCh) + keys, err := client.GetAllKeysForGuild(context.Background(), *guildId) + if err != nil { + panic(err) + } + + keyCh := make(chan string) + go func() { + for _, key := range keys { + keyCh <- key + } - objCh := client.ListObjectsV2(conf.Bucket, fmt.Sprintf("%d/", *guildId), true, doneCh) + close(keyCh) + }() - for obj := range objCh { - id := obj.Key - id = strings.Replace(id, fmt.Sprintf("%d/", *guildId), "", -1) - id = strings.Replace(id, "free-", "", -1) - parsed, err := strconv.Atoi(id) - must(err) + group, _ := errgroup.WithContext(context.Background()) + for i := 0; i < workers; i++ { + group.Go(func() error { + for key := range keyCh { + id := key[strings.LastIndex(key, "/")+1:] + parsed, err := strconv.Atoi(id) + must(err) - export(parsed, s) + if after != nil && *after > 0 && parsed < *after { + continue + } + + export(parsed, client) + } + + return nil + }) + } + + if err := group.Wait(); err != nil { + panic(err) } } } -func export(id int, s *http.Server) { - data, err := s.GetTicket(s.Config.Bucket, *guildId, id) +func export(id int, client *s3client.S3Client) { + data, err := client.GetTicket(context.Background(), *guildId, id) must(err) data, err = encryption.Decompress(data) diff --git a/cmd/exportuser/main.go b/cmd/exportuser/main.go index 4597bfa..8572b16 100644 --- a/cmd/exportuser/main.go +++ b/cmd/exportuser/main.go @@ -1,29 +1,34 @@ package main import ( - "bytes" "context" "encoding/json" + "errors" "flag" "fmt" "github.com/TicketsBot/common/encryption" "github.com/TicketsBot/database" "github.com/TicketsBot/logarchiver/pkg/config" - "github.com/TicketsBot/logarchiver/pkg/http" + "github.com/TicketsBot/logarchiver/pkg/model" + v1 "github.com/TicketsBot/logarchiver/pkg/model/v1" + v2 "github.com/TicketsBot/logarchiver/pkg/model/v2" + "github.com/TicketsBot/logarchiver/pkg/s3client" "github.com/jackc/pgx/v4/pgxpool" - "github.com/minio/minio-go/v6" + "github.com/minio/minio-go/v7" + "github.com/minio/minio-go/v7/pkg/credentials" "github.com/rxdn/gdl/cache" + "github.com/rxdn/gdl/objects/channel/message" + "go.uber.org/zap" "os" "strconv" "time" ) var ( - userId = flag.Uint64("userid", 0, "user id to export") - key = flag.String("key", "", "aes key") - fullTranscript = flag.Bool("fulltranscript", false, "export full transcript") // TODO: Implement - dbUri = flag.String("dburi", "", "database uri") - cacheUri = flag.String("cacheuri", "", "cache uri") + userId = flag.Uint64("userid", 0, "user id to export") + key = flag.String("key", "", "aes key") + dbUri = flag.String("dburi", "", "database uri") + cacheUri = flag.String("cacheuri", "", "cache uri") ) func main() { @@ -33,6 +38,8 @@ func main() { // likely to be file exists _ = os.Mkdir(fmt.Sprintf("export_user/%d", *userId), 0) + fmt.Println("Connecting to database...") + var db *database.Database { pool, err := pgxpool.Connect(context.Background(), *dbUri) @@ -41,6 +48,8 @@ func main() { db = database.NewDatabase(pool) } + fmt.Println("Connecting to cache...") + var c cache.PgCache { pool, err := pgxpool.Connect(context.Background(), *cacheUri) @@ -52,6 +61,8 @@ func main() { }) } + fmt.Println("Connected to cache") + // Get + write user data { userData := getUserData(db, *userId) @@ -115,38 +126,93 @@ func main() { func getTranscripts(conf config.Config, tickets map[uint64][]int) { // create minio client - client, err := minio.New(conf.Endpoint, conf.AccessKey, conf.SecretKey, false) + m, err := minio.New(conf.Endpoint, &minio.Options{ + Creds: credentials.NewStaticV4(conf.AccessKey, conf.SecretKey, ""), + }) if err != nil { panic(err) } - s := http.NewServer(conf, client) + client := s3client.NewS3Client(m, conf.Bucket) - doneCh := make(chan struct{}) - defer close(doneCh) + logger, err := zap.NewDevelopment() + if err != nil { + panic(err) + } _ = os.Mkdir(fmt.Sprintf("export_user/%d/transcripts", *userId), 0) for guildId, ticketIds := range tickets { for _, ticketId := range ticketIds { - data, err := s.GetTicket(conf.Bucket, guildId, ticketId) - must(err) + data, err := client.GetTicket(context.Background(), guildId, ticketId) + if err != nil { + if errors.Is(err, s3client.ErrTicketNotFound) { + logger.Info("ticket not found", zap.Uint64("guildId", guildId), zap.Int("ticketId", ticketId)) + continue + } else { + panic(err) + } + } data, err = encryption.Decompress(data) - must(err) + if err != nil { + logger.Error("failed to decompress", zap.Error(err), zap.Uint64("guildId", guildId), zap.Int("ticketId", ticketId)) + continue + } data, err = encryption.Decrypt([]byte(*key), data) - must(err) + if err != nil { + logger.Error("failed to decrypt", zap.Error(err), zap.Uint64("guildId", guildId), zap.Int("ticketId", ticketId)) + continue + } - var encoded bytes.Buffer - must(json.Indent(&encoded, data, "", " ")) + // Convert to v2 if needed + var transcript v2.Transcript + + version := model.GetVersion(data) + switch version { + case model.V1: + var messages []message.Message + if err := json.Unmarshal(data, &messages); err != nil { + panic(err) + } + + transcript = v1.ConvertToV2(messages) + case model.V2: + if err := json.Unmarshal(data, &transcript); err != nil { + panic(err) + } + default: + panic(fmt.Sprintf("Unknown version %d", version)) + } - f, err := os.Create(fmt.Sprintf("export_user/%d/transcripts/%d-%d.json", *userId, guildId, ticketId)) - must(err) + transcript.Entities.Channels = nil + transcript.Entities.Roles = nil + + user, ok := transcript.Entities.Users[*userId] + if !ok { + transcript.Entities.Users = nil + } else { + transcript.Entities.Users = map[uint64]v2.User{ + user.Id: user, + } + } + + var messages []v2.Message + for _, message := range transcript.Messages { + if message.AuthorId == *userId { + messages = append(messages, message) + } + } - _, err = encoded.WriteTo(f) + transcript.Messages = messages + + encoded, err := json.MarshalIndent(transcript, "", " ") must(err) + fileName := fmt.Sprintf("export_user/%d/transcripts/%d-%d.json", *userId, guildId, ticketId) + must(os.WriteFile(fileName, encoded, 0644)) + fmt.Printf("exported %d/%d\n", guildId, ticketId) } } diff --git a/cmd/fix-file-names/main.go b/cmd/fix-file-names/main.go new file mode 100644 index 0000000..1e6ab76 --- /dev/null +++ b/cmd/fix-file-names/main.go @@ -0,0 +1,88 @@ +package main + +import ( + "context" + "flag" + "github.com/TicketsBot/logarchiver/pkg/config" + "github.com/minio/minio-go/v7" + "github.com/minio/minio-go/v7/pkg/credentials" + "go.uber.org/zap" + "strings" + "sync" + "sync/atomic" +) + +const workers = 30 + +func main() { + flag.Parse() + conf := config.Parse() + + // create minio client + client, err := minio.New(conf.Endpoint, &minio.Options{ + Creds: credentials.NewStaticV4(conf.AccessKey, conf.SecretKey, ""), + }) + if err != nil { + panic(err) + } + + logger, err := zap.NewDevelopment() + if err != nil { + panic(err) + } + + doneCh := make(chan struct{}) + defer close(doneCh) + + ch := client.ListObjects(context.Background(), conf.Bucket, minio.ListObjectsOptions{ + Prefix: "", + Recursive: true, + }) + + var wg sync.WaitGroup + processed := atomic.Int32{} + freeCount := atomic.Int32{} + for i := 0; i < workers; i++ { + wg.Add(1) + + go func() { + for obj := range ch { + if strings.Contains(obj.Key, "free-") { + newName := strings.Replace(obj.Key, "free-", "", 1) + + src := minio.CopySrcOptions{ + Bucket: conf.Bucket, + Object: obj.Key, + } + + dst := minio.CopyDestOptions{ + Bucket: conf.Bucket, + Object: newName, + } + + if _, err := client.CopyObject(context.Background(), dst, src); err != nil { + logger.Fatal("Failed to copy object", zap.Error(err)) + continue + } + + if err := client.RemoveObject(context.Background(), conf.Bucket, obj.Key, minio.RemoveObjectOptions{}); err != nil { + logger.Fatal("Failed to remove object", zap.Error(err)) + continue + } + + updated := freeCount.Add(1) + logger.Info("Processed free ticket", zap.String("key", obj.Key), zap.Int32("processed", updated)) + } + + updated := processed.Add(1) + if updated%100_000 == 0 { + logger.Info("Processed objects", zap.Int32("processed", updated)) + } + } + + wg.Done() + }() + } + + wg.Wait() +} diff --git a/cmd/logarchiver/main.go b/cmd/logarchiver/main.go index 9c5b4a1..d09c177 100644 --- a/cmd/logarchiver/main.go +++ b/cmd/logarchiver/main.go @@ -6,7 +6,8 @@ import ( "github.com/TicketsBot/logarchiver/pkg/config" "github.com/TicketsBot/logarchiver/pkg/http" "github.com/getsentry/sentry-go" - "github.com/minio/minio-go/v6" + "github.com/minio/minio-go/v7" + "github.com/minio/minio-go/v7/pkg/credentials" "go.uber.org/zap" ) @@ -43,7 +44,9 @@ func main() { logger.Debug("Starting minio client...") // create minio client - client, err := minio.New(conf.Endpoint, conf.AccessKey, conf.SecretKey, false) + client, err := minio.New(conf.Endpoint, &minio.Options{ + Creds: credentials.NewStaticV4(conf.AccessKey, conf.SecretKey, ""), + }) if err != nil { logger.Fatal("Failed to create minio client", zap.Error(err), zap.String("endpoint", conf.Endpoint)) panic(err) // logger.Fatal should exit already diff --git a/cmd/purgemodmail/main.go b/cmd/purgemodmail/main.go deleted file mode 100644 index e0a28cc..0000000 --- a/cmd/purgemodmail/main.go +++ /dev/null @@ -1,32 +0,0 @@ -package main - -import ( - "flag" - "github.com/minio/minio-go/v6" - "strings" -) - -var ( - endpoint = flag.String("endpoint", "nyc3.digitaloceanspaces.com", "the S3 compatible object storage provider endpoint") - accessKey = flag.String("accesskey", "", "access key ID") - secretKey = flag.String("secretkey", "", "secret key") - bucket = flag.String("bucket", "", "the name of the bucket to manage") -) - -func main() { - flag.Parse() - - client, err := minio.New(*endpoint, *accessKey, *secretKey, false) - if err != nil { - panic(err) - } - - done := make(chan struct{}) - defer close(done) - - for object := range client.ListObjects(*bucket, "", true, done) { - if strings.Contains(object.Key, "modmail") { - client.RemoveObject(*bucket, object.Key) - } - } -} diff --git a/go.mod b/go.mod index e505e21..588f31b 100644 --- a/go.mod +++ b/go.mod @@ -1,28 +1,33 @@ module github.com/TicketsBot/logarchiver -go 1.18 +go 1.21 + +toolchain go1.22.4 require ( github.com/TicketsBot/common v0.0.0-20230723121853-8d873b27086e github.com/TicketsBot/database v0.0.0-20220217133004-d190910ad66f github.com/caarlos0/env v3.5.0+incompatible + github.com/getsentry/sentry-go v0.21.0 github.com/gin-contrib/zap v0.1.0 github.com/gin-gonic/gin v1.8.1 github.com/jackc/pgx/v4 v4.6.0 - github.com/minio/minio-go/v6 v6.0.53 github.com/rxdn/gdl v0.0.0-20230622203838-cad65ada73f0 go.uber.org/zap v1.23.0 + golang.org/x/sync v0.7.0 ) require ( github.com/boltdb/bolt v1.3.1 // indirect - github.com/getsentry/sentry-go v0.21.0 // indirect + github.com/dustin/go-humanize v1.0.1 // indirect github.com/gin-contrib/sse v0.1.0 // indirect + github.com/go-ini/ini v1.67.0 // indirect github.com/go-playground/locales v0.14.0 // indirect github.com/go-playground/universal-translator v0.18.0 // indirect github.com/go-playground/validator/v10 v10.11.1 // indirect - github.com/goccy/go-json v0.9.11 // indirect + github.com/goccy/go-json v0.10.3 // indirect github.com/gofrs/uuid v3.3.0+incompatible // indirect + github.com/google/uuid v1.6.0 // indirect github.com/jackc/chunkreader/v2 v2.0.1 // indirect github.com/jackc/pgconn v1.5.0 // indirect github.com/jackc/pgio v1.0.0 // indirect @@ -33,24 +38,28 @@ require ( github.com/jackc/pgx v3.6.2+incompatible // indirect github.com/jackc/puddle v1.1.0 // indirect github.com/json-iterator/go v1.1.12 // indirect - github.com/klauspost/compress v1.16.0 // indirect + github.com/klauspost/compress v1.17.9 // indirect + github.com/klauspost/cpuid/v2 v2.2.8 // indirect github.com/leodido/go-urn v1.2.1 // indirect github.com/mattn/go-isatty v0.0.17 // indirect + github.com/minio/md5-simd v1.1.2 // indirect + github.com/minio/minio-go/v7 v7.0.73 // indirect github.com/minio/sha256-simd v0.1.1 // indirect github.com/mitchellh/go-homedir v1.1.0 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/pelletier/go-toml/v2 v2.0.5 // indirect github.com/pkg/errors v0.9.1 // indirect + github.com/rs/xid v1.5.0 // indirect github.com/ugorji/go/codec v1.2.7 // indirect go.opentelemetry.io/otel v1.10.0 // indirect go.opentelemetry.io/otel/trace v1.10.0 // indirect go.uber.org/atomic v1.7.0 // indirect go.uber.org/multierr v1.6.0 // indirect - golang.org/x/crypto v0.7.0 // indirect - golang.org/x/net v0.8.0 // indirect - golang.org/x/sys v0.6.0 // indirect - golang.org/x/text v0.8.0 // indirect + golang.org/x/crypto v0.24.0 // indirect + golang.org/x/net v0.26.0 // indirect + golang.org/x/sys v0.21.0 // indirect + golang.org/x/text v0.16.0 // indirect golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect google.golang.org/protobuf v1.29.1 // indirect gopkg.in/ini.v1 v1.67.0 // indirect diff --git a/go.sum b/go.sum index c733ede..8b37ac7 100644 --- a/go.sum +++ b/go.sum @@ -18,6 +18,8 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= +github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/getsentry/sentry-go v0.21.0 h1:c9l5F1nPF30JIppulk4veau90PK6Smu3abgVtVQWon4= github.com/getsentry/sentry-go v0.21.0/go.mod h1:lc76E2QywIyW8WuBnwl8Lc4bkmQH4+w1gwTf25trprY= github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= @@ -27,6 +29,8 @@ github.com/gin-contrib/zap v0.1.0/go.mod h1:hvnZaPs478H1PGvRP8w89ZZbyJUiyip4ddiI github.com/gin-gonic/gin v1.8.1 h1:4+fr/el88TOO3ewCmQr8cx/CtZ/umlIRIs5M4NTNjf8= github.com/gin-gonic/gin v1.8.1/go.mod h1:ji8BvRH1azfM+SYow9zQ6SZMvR8qOMZHmsCuWR9tTTk= github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA= +github.com/go-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A= +github.com/go-ini/ini v1.67.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.2.3/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= @@ -43,6 +47,8 @@ github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/me github.com/goccy/go-json v0.9.7/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= github.com/goccy/go-json v0.9.11 h1:/pAaQDLHEoCq/5FFmSKBswWmK6H0e8g4159Kc/X/nqk= github.com/goccy/go-json v0.9.11/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= +github.com/goccy/go-json v0.10.3 h1:KZ5WoDbxAIgm2HNbYckL0se1fHD6rz5j4ywS6ebzDqA= +github.com/goccy/go-json v0.10.3/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= github.com/gofrs/uuid v3.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= github.com/gofrs/uuid v3.3.0+incompatible h1:8K4tyRfvU1CYPgJsveYFQMhpFd/wXNM7iK6rR7UHz84= github.com/gofrs/uuid v3.3.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= @@ -51,6 +57,8 @@ github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/jackc/chunkreader v1.0.0/go.mod h1:RT6O25fNZIuasFJRyZ4R/Y2BbhasbmZXF9QQ7T3kePo= github.com/jackc/chunkreader/v2 v2.0.0/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk= @@ -98,6 +106,11 @@ github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHm github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/klauspost/compress v1.16.0 h1:iULayQNOReoYUe+1qtKOqw9CwJv3aNQu8ivo7lw1HU4= github.com/klauspost/compress v1.16.0/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= +github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA= +github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= +github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= +github.com/klauspost/cpuid/v2 v2.2.8 h1:+StwCXwm9PdpiEkPyzBXIy+M9KUb4ODm0Zarf1kS5BM= +github.com/klauspost/cpuid/v2 v2.2.8/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= @@ -124,8 +137,12 @@ github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2y github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng= github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= -github.com/minio/minio-go/v6 v6.0.53 h1:8jzpwiOzZ5Iz7/goFWqNZRICbyWYShbb5rARjrnSCNI= -github.com/minio/minio-go/v6 v6.0.53/go.mod h1:DIvC/IApeHX8q1BAMVCXSXwpmrmM+I+iBvhvztQorfI= +github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34= +github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM= +github.com/minio/minio-go/v7 v6.0.53 h1:8jzpwiOzZ5Iz7/goFWqNZRICbyWYShbb5rARjrnSCNI= +github.com/minio/minio-go/v7 v6.0.53/go.mod h1:DIvC/IApeHX8q1BAMVCXSXwpmrmM+I+iBvhvztQorfI= +github.com/minio/minio-go/v7 v7.0.73 h1:qr2vi96Qm7kZ4v7LLebjte+MQh621fFWnv93p12htEo= +github.com/minio/minio-go/v7 v7.0.73/go.mod h1:qydcVzV8Hqtj1VtEocfxbmVFa2siu6HGa+LDEPogjD8= github.com/minio/sha256-simd v0.1.1 h1:5QHSlgo3nt5yKOJrC7W8w7X+NFl8cMPZm96iu8kKUJU= github.com/minio/sha256-simd v0.1.1/go.mod h1:B5e1o+1/KgNmWrSQK08Y6Z1Vb5pwIktudl0J58iy0KM= github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= @@ -149,6 +166,8 @@ github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTE github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8= github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE= github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ= +github.com/rs/xid v1.5.0 h1:mKX4bl4iPYJtEIxp6CYiUuLQ/8DYMoz0PUdtGgMFRVc= +github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU= github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc= github.com/rxdn/gdl v0.0.0-20230622203838-cad65ada73f0 h1:Oe8RWW9dHSYUsDjRUzNXgY+JIqjzJiHe1yFWzQptl7w= @@ -206,6 +225,8 @@ golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5y golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.7.0 h1:AvwMYaRytfdeVt3u6mLaxYtErKYjxA2OXjJ1HHq6t3A= golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= +golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI= +golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM= golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= @@ -218,8 +239,12 @@ golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96b golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.8.0 h1:Zrh2ngAOFYneWTAIAPethzeaQLuHwhuBkuV6ZiRnUaQ= golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= +golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ= +golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -236,8 +261,11 @@ golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= +golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= @@ -246,6 +274,8 @@ golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.8.0 h1:57P1ETyNKtuIjB4SRd15iJxuhj8Gc416Y78H3qgMh68= golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= +golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= diff --git a/pkg/http/purgeguild.go b/pkg/http/purgeguild.go index 9e05885..0e9b6d9 100644 --- a/pkg/http/purgeguild.go +++ b/pkg/http/purgeguild.go @@ -1,10 +1,12 @@ package http import ( + "context" "errors" "fmt" "github.com/TicketsBot/logarchiver/internal" "github.com/gin-gonic/gin" + "github.com/minio/minio-go/v7" "go.uber.org/zap" "net/http" "strconv" @@ -36,10 +38,12 @@ func (s *Server) purgeGuildHandler(ctx *gin.Context) { return } - removeCh := make(chan string) + removeCh := make(chan minio.ObjectInfo) go func() { + opts := minio.RemoveObjectsOptions{} + var errCount int - for err := range s.client.RemoveObjects(s.Config.Bucket, removeCh) { + for err := range s.minio.RemoveObjects(context.Background(), s.Config.Bucket, removeCh, opts) { s.RemoveQueue.AddError(guildId, err.ObjectName, err.Err) s.Logger.Error( @@ -66,10 +70,10 @@ func (s *Server) purgeGuildHandler(ctx *gin.Context) { }() go func() { - doneCh := make(chan struct{}) - defer close(doneCh) - - objCh := s.client.ListObjectsV2(s.Config.Bucket, fmt.Sprintf("%d/", guildId), true, doneCh) + objCh := s.minio.ListObjects(context.Background(), s.Config.Bucket, minio.ListObjectsOptions{ + Prefix: fmt.Sprintf("%d/", guildId), + Recursive: true, + }) for obj := range objCh { s.Logger.Info( @@ -79,7 +83,7 @@ func (s *Server) purgeGuildHandler(ctx *gin.Context) { ) s.RemoveQueue.AddRemovedObject(guildId, obj.Key) - removeCh <- obj.Key + removeCh <- obj } close(removeCh) diff --git a/pkg/http/s3.go b/pkg/http/s3.go deleted file mode 100644 index 90c73dc..0000000 --- a/pkg/http/s3.go +++ /dev/null @@ -1,67 +0,0 @@ -package http - -import ( - "bytes" - "errors" - "fmt" - "github.com/minio/minio-go/v6" -) - -var ErrTicketNotFound = errors.New("object not found") - -// data, error -func (s *Server) GetTicket(bucketName string, guildId uint64, ticketId int) ([]byte, error) { - // try reading with free name - reader, err := s.client.GetObject(bucketName, fmt.Sprintf("%d/free-%d", guildId, ticketId), minio.GetObjectOptions{}) - if err != nil { - return nil, err - } - - // if we found the free object, we can return it - if reader != nil { - defer reader.Close() - - var buff bytes.Buffer - _, err = buff.ReadFrom(reader) - if err != nil { - if err.Error() != "The specified key does not exist." { - return nil, err - } - } else { - return buff.Bytes(), nil - } - } - - // else, we should check the premium object - reader, err = s.client.GetObject(bucketName, fmt.Sprintf("%d/%d", guildId, ticketId), minio.GetObjectOptions{}) - if err != nil { - return nil, err - } - - if reader != nil { - defer reader.Close() - - var buff bytes.Buffer - _, err = buff.ReadFrom(reader) - if err != nil { - if err.Error() != "The specified key does not exist." { - return nil, err - } - } else { - return buff.Bytes(), nil - } - } - - return nil, ErrTicketNotFound -} - -func (s *Server) UploadTicket(bucket string, guildId uint64, ticketId int, data []byte) error { - name := fmt.Sprintf("%d/%d", guildId, ticketId) - - _, err := s.client.PutObject(bucket, name, bytes.NewReader(data), int64(len(data)), minio.PutObjectOptions{ - ContentType: "application/octet-stream", - ContentEncoding: "zstd", - }) - - return err -} diff --git a/pkg/http/server.go b/pkg/http/server.go index 04c232e..0ffebed 100644 --- a/pkg/http/server.go +++ b/pkg/http/server.go @@ -3,9 +3,10 @@ package http import ( "github.com/TicketsBot/logarchiver/internal" "github.com/TicketsBot/logarchiver/pkg/config" + "github.com/TicketsBot/logarchiver/pkg/s3client" ginzap "github.com/gin-contrib/zap" "github.com/gin-gonic/gin" - "github.com/minio/minio-go/v6" + "github.com/minio/minio-go/v7" "go.uber.org/zap" "time" ) @@ -15,7 +16,8 @@ type Server struct { Config config.Config RemoveQueue internal.RemoveQueue router *gin.Engine - client *minio.Client + minio *minio.Client + s3 *s3client.S3Client } func NewServer(logger *zap.Logger, config config.Config, client *minio.Client) *Server { @@ -24,7 +26,8 @@ func NewServer(logger *zap.Logger, config config.Config, client *minio.Client) * Config: config, RemoveQueue: internal.NewRemoveQueue(logger), router: gin.New(), - client: client, + minio: client, + s3: s3client.NewS3Client(client, config.Bucket), } } diff --git a/pkg/http/ticketget.go b/pkg/http/ticketget.go index 1e0db99..df3480e 100644 --- a/pkg/http/ticketget.go +++ b/pkg/http/ticketget.go @@ -1,6 +1,8 @@ package http import ( + "errors" + "github.com/TicketsBot/logarchiver/pkg/s3client" "github.com/gin-gonic/gin" "strconv" ) @@ -22,10 +24,10 @@ func (s *Server) ticketGetHandler(ctx *gin.Context) { return } - data, err := s.GetTicket(s.Config.Bucket, guild, id) + data, err := s.s3.GetTicket(ctx, guild, id) if err != nil { var statusCode int - if err == ErrTicketNotFound { + if errors.Is(err, s3client.ErrTicketNotFound) { statusCode = 404 } else { statusCode = 500 diff --git a/pkg/http/ticketupload.go b/pkg/http/ticketupload.go index 4ae200d..9456323 100644 --- a/pkg/http/ticketupload.go +++ b/pkg/http/ticketupload.go @@ -30,7 +30,7 @@ func (s *Server) ticketUploadHandler(ctx *gin.Context) { return } - if err := s.UploadTicket(s.Config.Bucket, guild, id, body); err != nil { + if err := s.s3.StoreTicket(ctx, guild, id, body); err != nil { ctx.JSON(500, gin.H{ "message": err.Error(), }) diff --git a/pkg/s3client/client.go b/pkg/s3client/client.go new file mode 100644 index 0000000..48f89d5 --- /dev/null +++ b/pkg/s3client/client.go @@ -0,0 +1,81 @@ +package s3client + +import ( + "bytes" + "context" + "errors" + "fmt" + "github.com/minio/minio-go/v7" +) + +type S3Client struct { + client *minio.Client + bucketName string +} + +func NewS3Client(client *minio.Client, bucketName string) *S3Client { + return &S3Client{ + client: client, + bucketName: bucketName, + } +} + +func (c *S3Client) GetTicket(ctx context.Context, guildId uint64, ticketId int) ([]byte, error) { + key := fmt.Sprintf("%d/%d", guildId, ticketId) + + object, err := c.client.GetObject(ctx, c.bucketName, key, minio.GetObjectOptions{}) + if err != nil { + var resp minio.ErrorResponse + if errors.As(err, &resp) { + if resp.Code == "NoSuchKey" { + return nil, ErrTicketNotFound + } + } + + return nil, err + } + + defer object.Close() + + var buff bytes.Buffer + if _, err := buff.ReadFrom(object); err != nil { + return nil, err + } + + return buff.Bytes(), nil +} + +func (c *S3Client) StoreTicket(ctx context.Context, guildId uint64, ticketId int, data []byte) error { + key := fmt.Sprintf("%d/%d", guildId, ticketId) + + _, err := c.client.PutObject(ctx, c.bucketName, key, bytes.NewReader(data), int64(len(data)), minio.PutObjectOptions{ + ContentType: "application/octet-stream", + ContentEncoding: "zstd", + }) + + return err +} + +func (c *S3Client) DeleteTicket(ctx context.Context, guildId uint64, ticketId int) error { + key := fmt.Sprintf("%d/%d", guildId, ticketId) + + return c.client.RemoveObject(ctx, c.bucketName, key, minio.RemoveObjectOptions{}) +} + +// GetAllKeysForGuild returns all keys in the bucket for a given guild. This can be a very slow operation, and so +// is only recommended for use in manual scripts. +func (c *S3Client) GetAllKeysForGuild(ctx context.Context, guildId uint64) ([]string, error) { + prefix := fmt.Sprintf("%d/", guildId) + opts := minio.ListObjectsOptions{ + WithMetadata: true, + Prefix: prefix, + Recursive: true, + } + + keys := make([]string, 0) + for obj := range c.client.ListObjects(ctx, c.bucketName, opts) { + keys = append(keys, obj.Key) + } + + return keys, nil +} diff --git a/pkg/s3client/errors.go b/pkg/s3client/errors.go new file mode 100644 index 0000000..66abf2b --- /dev/null +++ b/pkg/s3client/errors.go @@ -0,0 +1,5 @@ +package s3client + +import "errors" + +var ErrTicketNotFound = errors.New("object not found")