mirror of
https://github.com/KDE/kde-linux.git
synced 2026-04-19 22:19:24 -04:00
197 lines
5.0 KiB
Go
197 lines
5.0 KiB
Go
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
|
|
// SPDX-FileCopyrightText: 2024-2025 Harald Sitter <sitter@kde.org>
|
|
|
|
package main
|
|
|
|
import (
|
|
"context"
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"errors"
|
|
"flag"
|
|
"io"
|
|
"log"
|
|
"net/url"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"sync"
|
|
|
|
"github.com/minio/minio-go/v7"
|
|
"github.com/minio/minio-go/v7/pkg/credentials"
|
|
)
|
|
|
|
func connectToMinIO(endpoint string) *minio.Client {
|
|
awsSection, err := readConfigAWS("default")
|
|
if err != nil {
|
|
log.Fatalln("Failed to read AWS config:", err)
|
|
}
|
|
accessKeyID := awsSection.AccessKeyId
|
|
if accessKeyID == "" {
|
|
log.Fatalln("AWS access key ID is empty")
|
|
}
|
|
secretAccessKey := awsSection.SecretKey
|
|
if secretAccessKey == "" {
|
|
log.Fatalln("AWS secret access key is empty")
|
|
}
|
|
sessionToken := awsSection.SessionToken
|
|
if secretAccessKey == "" {
|
|
log.Fatalln("AWS session token is empty")
|
|
}
|
|
useSSL := true
|
|
|
|
// Initialize minio client object.
|
|
minioClient, err := minio.New(endpoint, &minio.Options{
|
|
Creds: credentials.NewStaticV4(accessKeyID, secretAccessKey, sessionToken),
|
|
Secure: useSSL,
|
|
TrailingHeaders: true,
|
|
})
|
|
if err != nil {
|
|
log.Fatalln("Failed to create MinIO client:", err)
|
|
}
|
|
|
|
buckets, err := minioClient.ListBuckets(context.Background())
|
|
if err != nil {
|
|
log.Fatalln("Failed to list buckets:", err)
|
|
}
|
|
for _, bucket := range buckets {
|
|
log.Println(bucket)
|
|
}
|
|
|
|
return minioClient
|
|
}
|
|
|
|
func sha256File(path string) string {
|
|
file, err := os.Open(path)
|
|
if err != nil {
|
|
log.Fatalf("unable to open file %s: %v", path, err)
|
|
}
|
|
defer file.Close()
|
|
|
|
hasher := sha256.New()
|
|
if _, err := io.Copy(hasher, file); err != nil {
|
|
log.Fatalf("unable to hash file %s: %v", path, err)
|
|
}
|
|
return hex.EncodeToString(hasher.Sum(nil))
|
|
}
|
|
|
|
type UploadObject struct {
|
|
ObjectName string
|
|
Path string
|
|
SHA256 string
|
|
}
|
|
|
|
func upload(client *minio.Client, bucket string, objectNamePrefix string) {
|
|
dir := "../upload-tree"
|
|
|
|
// Somewhat involved producer-consumer going on here.
|
|
// - We start N sha256 calculation workers.
|
|
// - We then feed the calcs channel with partial UploadObjects.
|
|
// - Close the calcs channel to eventually let the workers drain and finish.
|
|
// - The workers calculate the sha and then push the complete objects into the results channel.
|
|
// - Once the calcs waitgroup is done we close the results channel as well to let the uploader drain and finish.
|
|
|
|
calcs := make(chan UploadObject)
|
|
results := make(chan UploadObject)
|
|
|
|
calcsWg := sync.WaitGroup{}
|
|
|
|
// The sha256 calculation workers.
|
|
for i := 0; i < 4; i++ {
|
|
calcsWg.Add(1)
|
|
go func() {
|
|
defer calcsWg.Done()
|
|
for obj := range calcs {
|
|
obj.SHA256 = sha256File(obj.Path)
|
|
results <- obj
|
|
}
|
|
}()
|
|
}
|
|
|
|
// Once they are all done we can close the results channel.
|
|
go func() {
|
|
calcsWg.Wait()
|
|
close(results)
|
|
}()
|
|
|
|
// The calcs channel gets fed by the walking of the upload tree.
|
|
err := filepath.WalkDir(dir, func(path string, d os.DirEntry, err error) error {
|
|
if err != nil {
|
|
return err
|
|
}
|
|
objectName, err := filepath.Rel(dir, path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
objectName = filepath.Join(objectNamePrefix, objectName)
|
|
if objectName == "" {
|
|
return errors.New("object name cannot be empty")
|
|
}
|
|
|
|
if d.IsDir() {
|
|
return nil
|
|
}
|
|
|
|
calcs <- UploadObject{
|
|
ObjectName: objectName,
|
|
Path: path,
|
|
}
|
|
|
|
return nil
|
|
})
|
|
close(calcs)
|
|
if err != nil {
|
|
log.Fatalln(err)
|
|
}
|
|
|
|
// Upload. This loop only ends when results is closed and drained.
|
|
for res := range results {
|
|
client.GetObjectAttributes(context.Background(), bucket, res.ObjectName, minio.ObjectAttributesOptions{})
|
|
log.Println("Uploading", res.ObjectName, "from", res.Path)
|
|
info, err := client.FPutObject(context.Background(), bucket, res.ObjectName, res.Path, minio.PutObjectOptions{
|
|
UserMetadata: map[string]string{
|
|
"X-KDE-SHA256": res.SHA256,
|
|
},
|
|
})
|
|
if err != nil {
|
|
log.Fatalln(err)
|
|
}
|
|
log.Println("Uploaded", res.ObjectName, "of size", info.Size, "ETag", info.ETag, "VersionID", info.VersionID, "SHA256", info.ChecksumSHA256, "Metadata", info)
|
|
}
|
|
}
|
|
|
|
func main() {
|
|
remote := flag.String("remote", "", "remote url to upload to, e.g. s3+https://storage.kde.org/kde-linux/sysupdate/v2/store")
|
|
flag.Parse()
|
|
|
|
remoteURI, err := url.Parse(*remote)
|
|
if err != nil {
|
|
log.Fatalln("Failed to parse remote URL:", err)
|
|
}
|
|
if remoteURI.Scheme != "s3+https" {
|
|
log.Fatalln("Unsupported remote scheme:", remoteURI.Scheme)
|
|
}
|
|
if remoteURI.Host != "storage.kde.org" {
|
|
log.Fatalln("Unsupported remote host:", remoteURI.Host)
|
|
}
|
|
parts := strings.SplitN(remoteURI.Path[1:], "/", 2)
|
|
if len(parts) != 2 {
|
|
log.Fatalln("Invalid remote path, expected format: /bucket/path")
|
|
}
|
|
bucket := parts[0]
|
|
path := parts[1]
|
|
if bucket == "" {
|
|
log.Fatalln("Invalid remote path, expected format: /bucket/path")
|
|
}
|
|
if path == "" {
|
|
log.Println("Warning: path is empty, uploading to bucket root")
|
|
path = "/"
|
|
}
|
|
|
|
log.Println("Connecting to MinIO at", remoteURI.Host)
|
|
minioClient := connectToMinIO(remoteURI.Host)
|
|
|
|
log.Println("Uploading to bucket", bucket, "with path prefix", path)
|
|
upload(minioClient, bucket, path)
|
|
}
|