aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--cmd/xedn/uplodr/generate.go5
-rw-r--r--cmd/xedn/uplodr/main.go307
-rw-r--r--cmd/xedn/uplodr/pb/uplodr.pb.go379
-rw-r--r--cmd/xedn/uplodr/pb/uplodr_grpc.pb.go146
-rw-r--r--cmd/xedn/uplodr/uplodr.proto27
-rw-r--r--internal/tigris.go21
6 files changed, 885 insertions, 0 deletions
diff --git a/cmd/xedn/uplodr/generate.go b/cmd/xedn/uplodr/generate.go
new file mode 100644
index 0000000..207e2c9
--- /dev/null
+++ b/cmd/xedn/uplodr/generate.go
@@ -0,0 +1,5 @@
+package main
+
+func init() {}
+
+//go:generate protoc --proto_path=. --go_out=./pb --go_opt=paths=source_relative --go-grpc_out=./pb --go-grpc_opt=paths=source_relative uplodr.proto
diff --git a/cmd/xedn/uplodr/main.go b/cmd/xedn/uplodr/main.go
new file mode 100644
index 0000000..bfca3aa
--- /dev/null
+++ b/cmd/xedn/uplodr/main.go
@@ -0,0 +1,307 @@
+package main
+
+import (
+ "bytes"
+ "context"
+ "errors"
+ "flag"
+ "fmt"
+ "image"
+ "image/jpeg"
+ "image/png"
+ "log"
+ "log/slog"
+ "net"
+ "os"
+ "path/filepath"
+ "runtime"
+ "time"
+
+ "github.com/aws/aws-sdk-go-v2/aws"
+ "github.com/aws/aws-sdk-go-v2/config"
+ "github.com/aws/aws-sdk-go-v2/credentials"
+ "github.com/aws/aws-sdk-go-v2/service/s3"
+ "github.com/chai2010/webp"
+ "github.com/disintegration/imaging"
+ "google.golang.org/grpc"
+ "within.website/x/cmd/xedn/uplodr/pb"
+ "within.website/x/internal"
+ "within.website/x/internal/avif"
+)
+
+var (
+ grpcAddr = flag.String("grpc-addr", ":8080", "address to listen on for GRPC")
+
+ b2Bucket = flag.String("b2-bucket", "christine-static", "Backblaze B2 bucket to dump things to")
+ b2KeyID = flag.String("b2-key-id", "", "Backblaze B2 application key ID")
+ b2KeySecret = flag.String("b2-application-key", "", "Backblaze B2 application secret")
+
+ tigrisBucket = flag.String("bucket-name", "xedn", "Tigris bucket to dump things to")
+
+ avifQuality = flag.Int("avif-quality", 8, "AVIF quality (higher is worse quality)")
+ avifEncoderSpeed = flag.Int("avif-encoder-speed", 0, "AVIF encoder speed (higher is faster)")
+
+ jpegQuality = flag.Int("jpeg-quality", 90, "JPEG quality (lower means lower file size)")
+
+ webpQuality = flag.Int("webp-quality", 9, "WEBP quality (higher is worse quality)")
+)
+
+func main() {
+ internal.HandleStartup()
+ ctx, cancel := context.WithTimeout(context.Background(), 20*time.Minute)
+ defer cancel()
+
+ s, err := New(ctx)
+ if err != nil {
+ log.Fatal(err)
+ }
+
+ go func() {
+ <-ctx.Done()
+ panic("timeout")
+ }()
+
+ lis, err := net.Listen("tcp", *grpcAddr)
+ if err != nil {
+ log.Fatal(err)
+ }
+
+ gs := grpc.NewServer()
+
+ pb.RegisterImageServer(gs, s)
+
+ log.Fatal(gs.Serve(lis))
+}
+
+type Server struct {
+ tc *s3.Client
+ b2c *s3.Client
+
+ pb.UnimplementedImageServer
+}
+
+func New(ctx context.Context) (*Server, error) {
+ tc, err := mkTigrisClient(ctx)
+ if err != nil {
+ return nil, fmt.Errorf("failed to create Tigris client: %w", err)
+ }
+
+ b2c := mkB2Client()
+
+ return &Server{
+ tc: tc,
+ b2c: b2c,
+ }, nil
+}
+
+func (s *Server) Ping(ctx context.Context, msg *pb.Echo) (*pb.Echo, error) {
+ return msg, nil
+}
+
+func (s *Server) Upload(ctx context.Context, ur *pb.UploadReq) (*pb.UploadResp, error) {
+ img, format, err := image.Decode(bytes.NewBuffer(ur.Data))
+ if err != nil {
+ slog.Error("can't decode image", "err", err, "filename", ur.GetFileName())
+ return nil, err
+ }
+ slog.Debug("got image", "format", format)
+
+ baseName := fileNameWithoutExt(ur.GetFileName())
+
+ fnames := []string{}
+
+ dir, err := os.MkdirTemp("", "xedn-uplodr")
+ if err != nil {
+ return nil, fmt.Errorf("failed to create temp dir: %w")
+ }
+ defer os.RemoveAll(dir)
+
+ name := baseName + "-smol.png"
+ fnames = append(fnames, name)
+ if err := resizeSmol(img, filepath.Join(dir, name)); err != nil {
+ return nil, fmt.Errorf("failed to make smol png: %w", err)
+ }
+
+ name = baseName + ".webp"
+ fnames = append(fnames, filepath.Join(dir, name))
+ if err := doWEBP(img, name); err != nil {
+ return nil, fmt.Errorf("failed to make webp: %w", err)
+ }
+
+ name = baseName + ".avif"
+ fnames = append(fnames, filepath.Join(dir, name))
+ if err := doAVIF(img, name); err != nil {
+ return nil, fmt.Errorf("failed to make avif: %w", err)
+ }
+
+ name = baseName + ".jpg"
+ fnames = append(fnames, filepath.Join(dir, name))
+ if err := doJPEG(img, name); err != nil {
+ return nil, fmt.Errorf("failed to make jpeg: %w", err)
+ }
+
+ var result []*pb.Variant
+
+ errs := []error{}
+ for _, fname := range fnames {
+ path := filepath.Join(dir, fname)
+ slog.Info("uploading", "path", path)
+
+ fin, err := os.Open(path)
+ if err != nil {
+ slog.Error("can't open file", "path", path, "err", err)
+ errs = append(errs, fmt.Errorf("while uploading %s: %w", path, err))
+ continue
+ }
+ defer fin.Close()
+
+ if _, err := s.b2c.PutObject(ctx, &s3.PutObjectInput{
+ Body: fin,
+ Bucket: b2Bucket,
+ Key: aws.String(fmt.Sprintf("%s/%s", ur.Folder, fname)),
+ ContentType: aws.String(mimeTypes[filepath.Ext(fname)]),
+ }); err != nil {
+ slog.Error("can't upload", "to", "b2", "err", err)
+ errs = append(errs, fmt.Errorf("while uploading %s to b2: %w", path, err))
+ continue
+ }
+
+ fin.Seek(0, 0)
+
+ if _, err := s.tc.PutObject(ctx, &s3.PutObjectInput{
+ Body: fin,
+ Bucket: b2Bucket,
+ Key: aws.String(fmt.Sprintf("%s/%s", ur.Folder, fname)),
+ ContentType: aws.String(mimeTypes[filepath.Ext(fname)]),
+ }); err != nil {
+ slog.Error("can't upload", "to", "b2", "err", err)
+ errs = append(errs, fmt.Errorf("while uploading %s to b2: %w", path, err))
+ continue
+ }
+
+ result = append(result, &pb.Variant{
+ Url: fmt.Sprintf("https://cdn.xeiaso.net/file/christine-static/%s/%s", ur.GetFolder(), fname),
+ MimeType: mimeTypes[filepath.Ext(fname)],
+ })
+ }
+
+ if len(errs) != 0 {
+ return nil, errors.Join(errs...)
+ }
+
+ return &pb.UploadResp{
+ Variants: result,
+ }, nil
+}
+
+func doAVIF(src image.Image, dstPath string) error {
+ dst, err := os.Create(dstPath)
+ if err != nil {
+ log.Fatalf("Can't create destination file: %v", err)
+ }
+ defer dst.Close()
+
+ err = avif.Encode(dst, src, &avif.Options{
+ Threads: runtime.GOMAXPROCS(0),
+ Speed: *avifEncoderSpeed,
+ Quality: *avifQuality,
+ })
+ if err != nil {
+ return err
+ }
+
+ log.Printf("Encoded AVIF at %s", dstPath)
+
+ return nil
+}
+
+func doWEBP(src image.Image, dstPath string) error {
+ fout, err := os.Create(dstPath)
+ if err != nil {
+ return err
+ }
+ defer fout.Close()
+
+ err = webp.Encode(fout, src, &webp.Options{Quality: float32(*webpQuality)})
+ if err != nil {
+ return err
+ }
+
+ log.Printf("Encoded WEBP at %s", dstPath)
+
+ return nil
+}
+
+func fileNameWithoutExt(fileName string) string {
+ return filepath.Base(fileName[:len(fileName)-len(filepath.Ext(fileName))])
+}
+
+func doJPEG(src image.Image, dstPath string) error {
+ fout, err := os.Create(dstPath)
+ if err != nil {
+ return err
+ }
+ defer fout.Close()
+
+ if err := jpeg.Encode(fout, src, &jpeg.Options{Quality: *jpegQuality}); err != nil {
+ return err
+ }
+
+ log.Printf("Encoded JPEG at %s", dstPath)
+
+ return nil
+}
+
+func resizeSmol(src image.Image, dstPath string) error {
+ fout, err := os.Create(dstPath)
+ if err != nil {
+ return err
+ }
+ defer fout.Close()
+
+ dstImg := imaging.Resize(src, 800, 0, imaging.Lanczos)
+
+ enc := png.Encoder{
+ CompressionLevel: png.BestCompression,
+ }
+
+ if err := enc.Encode(fout, dstImg); err != nil {
+ return err
+ }
+
+ log.Printf("Encoded smol PNG at %s", dstPath)
+
+ return nil
+}
+
+var mimeTypes = map[string]string{
+ ".avif": "image/avif",
+ ".webp": "image/webp",
+ ".jpg": "image/jpeg",
+ ".png": "image/png",
+ ".wasm": "application/wasm",
+ ".css": "text/css",
+}
+
+func mkTigrisClient(ctx context.Context) (*s3.Client, error) {
+ cfg, err := config.LoadDefaultConfig(ctx)
+ if err != nil {
+ return nil, fmt.Errorf("failed to load Tigris config: %w", err)
+ }
+
+ return s3.NewFromConfig(cfg, func(o *s3.Options) {
+ o.BaseEndpoint = aws.String("https://fly.storage.tigris.dev")
+ }), nil
+}
+
+func mkB2Client() *s3.Client {
+ s3Config := aws.Config{
+ Credentials: credentials.NewStaticCredentialsProvider(*b2KeyID, *b2KeySecret, ""),
+ BaseEndpoint: aws.String("https://s3.us-west-001.backblazeb2.com"),
+ Region: "us-west-001",
+ }
+ s3Client := s3.NewFromConfig(s3Config, (func(o *s3.Options) {
+ o.UsePathStyle = true
+ }))
+ return s3Client
+}
diff --git a/cmd/xedn/uplodr/pb/uplodr.pb.go b/cmd/xedn/uplodr/pb/uplodr.pb.go
new file mode 100644
index 0000000..b496363
--- /dev/null
+++ b/cmd/xedn/uplodr/pb/uplodr.pb.go
@@ -0,0 +1,379 @@
+// Code generated by protoc-gen-go. DO NOT EDIT.
+// versions:
+// protoc-gen-go v1.32.0
+// protoc v4.23.4
+// source: uplodr.proto
+
+package pb
+
+import (
+ protoreflect "google.golang.org/protobuf/reflect/protoreflect"
+ protoimpl "google.golang.org/protobuf/runtime/protoimpl"
+ reflect "reflect"
+ sync "sync"
+)
+
+const (
+ // Verify that this generated code is sufficiently up-to-date.
+ _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
+ // Verify that runtime/protoimpl is sufficiently up-to-date.
+ _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
+)
+
+type UploadReq struct {
+ state protoimpl.MessageState
+ sizeCache protoimpl.SizeCache
+ unknownFields protoimpl.UnknownFields
+
+ FileName string `protobuf:"bytes,1,opt,name=file_name,json=fileName,proto3" json:"file_name,omitempty"`
+ Data []byte `protobuf:"bytes,2,opt,name=data,proto3" json:"data,omitempty"`
+ Folder string `protobuf:"bytes,3,opt,name=folder,proto3" json:"folder,omitempty"`
+}
+
+func (x *UploadReq) Reset() {
+ *x = UploadReq{}
+ if protoimpl.UnsafeEnabled {
+ mi := &file_uplodr_proto_msgTypes[0]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+ }
+}
+
+func (x *UploadReq) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*UploadReq) ProtoMessage() {}
+
+func (x *UploadReq) ProtoReflect() protoreflect.Message {
+ mi := &file_uplodr_proto_msgTypes[0]
+ if protoimpl.UnsafeEnabled && x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use UploadReq.ProtoReflect.Descriptor instead.
+func (*UploadReq) Descriptor() ([]byte, []int) {
+ return file_uplodr_proto_rawDescGZIP(), []int{0}
+}
+
+func (x *UploadReq) GetFileName() string {
+ if x != nil {
+ return x.FileName
+ }
+ return ""
+}
+
+func (x *UploadReq) GetData() []byte {
+ if x != nil {
+ return x.Data
+ }
+ return nil
+}
+
+func (x *UploadReq) GetFolder() string {
+ if x != nil {
+ return x.Folder
+ }
+ return ""
+}
+
+type UploadResp struct {
+ state protoimpl.MessageState
+ sizeCache protoimpl.SizeCache
+ unknownFields protoimpl.UnknownFields
+
+ Variants []*Variant `protobuf:"bytes,1,rep,name=variants,proto3" json:"variants,omitempty"`
+}
+
+func (x *UploadResp) Reset() {
+ *x = UploadResp{}
+ if protoimpl.UnsafeEnabled {
+ mi := &file_uplodr_proto_msgTypes[1]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+ }
+}
+
+func (x *UploadResp) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*UploadResp) ProtoMessage() {}
+
+func (x *UploadResp) ProtoReflect() protoreflect.Message {
+ mi := &file_uplodr_proto_msgTypes[1]
+ if protoimpl.UnsafeEnabled && x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use UploadResp.ProtoReflect.Descriptor instead.
+func (*UploadResp) Descriptor() ([]byte, []int) {
+ return file_uplodr_proto_rawDescGZIP(), []int{1}
+}
+
+func (x *UploadResp) GetVariants() []*Variant {
+ if x != nil {
+ return x.Variants
+ }
+ return nil
+}
+
+type Variant struct {
+ state protoimpl.MessageState
+ sizeCache protoimpl.SizeCache
+ unknownFields protoimpl.UnknownFields
+
+ Url string `protobuf:"bytes,1,opt,name=url,proto3" json:"url,omitempty"`
+ MimeType string `protobuf:"bytes,2,opt,name=mime_type,json=mimeType,proto3" json:"mime_type,omitempty"`
+}
+
+func (x *Variant) Reset() {
+ *x = Variant{}
+ if protoimpl.UnsafeEnabled {
+ mi := &file_uplodr_proto_msgTypes[2]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+ }
+}
+
+func (x *Variant) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*Variant) ProtoMessage() {}
+
+func (x *Variant) ProtoReflect() protoreflect.Message {
+ mi := &file_uplodr_proto_msgTypes[2]
+ if protoimpl.UnsafeEnabled && x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use Variant.ProtoReflect.Descriptor instead.
+func (*Variant) Descriptor() ([]byte, []int) {
+ return file_uplodr_proto_rawDescGZIP(), []int{2}
+}
+
+func (x *Variant) GetUrl() string {
+ if x != nil {
+ return x.Url
+ }
+ return ""
+}
+
+func (x *Variant) GetMimeType() string {
+ if x != nil {
+ return x.MimeType
+ }
+ return ""
+}
+
+type Echo struct {
+ state protoimpl.MessageState
+ sizeCache protoimpl.SizeCache
+ unknownFields protoimpl.UnknownFields
+
+ Nonce string `protobuf:"bytes,1,opt,name=nonce,proto3" json:"nonce,omitempty"`
+}
+
+func (x *Echo) Reset() {
+ *x = Echo{}
+ if protoimpl.UnsafeEnabled {
+ mi := &file_uplodr_proto_msgTypes[3]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+ }
+}
+
+func (x *Echo) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*Echo) ProtoMessage() {}
+
+func (x *Echo) ProtoReflect() protoreflect.Message {
+ mi := &file_uplodr_proto_msgTypes[3]
+ if protoimpl.UnsafeEnabled && x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use Echo.ProtoReflect.Descriptor instead.
+func (*Echo) Descriptor() ([]byte, []int) {
+ return file_uplodr_proto_rawDescGZIP(), []int{3}
+}
+
+func (x *Echo) GetNonce() string {
+ if x != nil {
+ return x.Nonce
+ }
+ return ""
+}
+
+var File_uplodr_proto protoreflect.FileDescriptor
+
+var file_uplodr_proto_rawDesc = []byte{
+ 0x0a, 0x0c, 0x75, 0x70, 0x6c, 0x6f, 0x64, 0x72, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x1c,
+ 0x77, 0x69, 0x74, 0x68, 0x69, 0x6e, 0x2e, 0x77, 0x65, 0x62, 0x73, 0x69, 0x74, 0x65, 0x2e, 0x78,
+ 0x2e, 0x78, 0x65, 0x64, 0x6e, 0x2e, 0x75, 0x70, 0x6c, 0x6f, 0x64, 0x72, 0x22, 0x54, 0x0a, 0x09,
+ 0x55, 0x70, 0x6c, 0x6f, 0x61, 0x64, 0x52, 0x65, 0x71, 0x12, 0x1b, 0x0a, 0x09, 0x66, 0x69, 0x6c,
+ 0x65, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x66, 0x69,
+ 0x6c, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x64, 0x61, 0x74, 0x61, 0x18, 0x02,
+ 0x20, 0x01, 0x28, 0x0c, 0x52, 0x04, 0x64, 0x61, 0x74, 0x61, 0x12, 0x16, 0x0a, 0x06, 0x66, 0x6f,
+ 0x6c, 0x64, 0x65, 0x72, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x66, 0x6f, 0x6c, 0x64,
+ 0x65, 0x72, 0x22, 0x4f, 0x0a, 0x0a, 0x55, 0x70, 0x6c, 0x6f, 0x61, 0x64, 0x52, 0x65, 0x73, 0x70,
+ 0x12, 0x41, 0x0a, 0x08, 0x76, 0x61, 0x72, 0x69, 0x61, 0x6e, 0x74, 0x73, 0x18, 0x01, 0x20, 0x03,
+ 0x28, 0x0b, 0x32, 0x25, 0x2e, 0x77, 0x69, 0x74, 0x68, 0x69, 0x6e, 0x2e, 0x77, 0x65, 0x62, 0x73,
+ 0x69, 0x74, 0x65, 0x2e, 0x78, 0x2e, 0x78, 0x65, 0x64, 0x6e, 0x2e, 0x75, 0x70, 0x6c, 0x6f, 0x64,
+ 0x72, 0x2e, 0x56, 0x61, 0x72, 0x69, 0x61, 0x6e, 0x74, 0x52, 0x08, 0x76, 0x61, 0x72, 0x69, 0x61,
+ 0x6e, 0x74, 0x73, 0x22, 0x38, 0x0a, 0x07, 0x56, 0x61, 0x72, 0x69, 0x61, 0x6e, 0x74, 0x12, 0x10,
+ 0x0a, 0x03, 0x75, 0x72, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x75, 0x72, 0x6c,
+ 0x12, 0x1b, 0x0a, 0x09, 0x6d, 0x69, 0x6d, 0x65, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, 0x02, 0x20,
+ 0x01, 0x28, 0x09, 0x52, 0x08, 0x6d, 0x69, 0x6d, 0x65, 0x54, 0x79, 0x70, 0x65, 0x22, 0x1c, 0x0a,
+ 0x04, 0x45, 0x63, 0x68, 0x6f, 0x12, 0x14, 0x0a, 0x05, 0x6e, 0x6f, 0x6e, 0x63, 0x65, 0x18, 0x01,
+ 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x6e, 0x6f, 0x6e, 0x63, 0x65, 0x32, 0xb4, 0x01, 0x0a, 0x05,
+ 0x49, 0x6d, 0x61, 0x67, 0x65, 0x12, 0x4e, 0x0a, 0x04, 0x50, 0x69, 0x6e, 0x67, 0x12, 0x22, 0x2e,
+ 0x77, 0x69, 0x74, 0x68, 0x69, 0x6e, 0x2e, 0x77, 0x65, 0x62, 0x73, 0x69, 0x74, 0x65, 0x2e, 0x78,
+ 0x2e, 0x78, 0x65, 0x64, 0x6e, 0x2e, 0x75, 0x70, 0x6c, 0x6f, 0x64, 0x72, 0x2e, 0x45, 0x63, 0x68,
+ 0x6f, 0x1a, 0x22, 0x2e, 0x77, 0x69, 0x74, 0x68, 0x69, 0x6e, 0x2e, 0x77, 0x65, 0x62, 0x73, 0x69,
+ 0x74, 0x65, 0x2e, 0x78, 0x2e, 0x78, 0x65, 0x64, 0x6e, 0x2e, 0x75, 0x70, 0x6c, 0x6f, 0x64, 0x72,
+ 0x2e, 0x45, 0x63, 0x68, 0x6f, 0x12, 0x5b, 0x0a, 0x06, 0x55, 0x70, 0x6c, 0x6f, 0x61, 0x64, 0x12,
+ 0x27, 0x2e, 0x77, 0x69, 0x74, 0x68, 0x69, 0x6e, 0x2e, 0x77, 0x65, 0x62, 0x73, 0x69, 0x74, 0x65,
+ 0x2e, 0x78, 0x2e, 0x78, 0x65, 0x64, 0x6e, 0x2e, 0x75, 0x70, 0x6c, 0x6f, 0x64, 0x72, 0x2e, 0x55,
+ 0x70, 0x6c, 0x6f, 0x61, 0x64, 0x52, 0x65, 0x71, 0x1a, 0x28, 0x2e, 0x77, 0x69, 0x74, 0x68, 0x69,
+ 0x6e, 0x2e, 0x77, 0x65, 0x62, 0x73, 0x69, 0x74, 0x65, 0x2e, 0x78, 0x2e, 0x78, 0x65, 0x64, 0x6e,
+ 0x2e, 0x75, 0x70, 0x6c, 0x6f, 0x64, 0x72, 0x2e, 0x55, 0x70, 0x6c, 0x6f, 0x61, 0x64, 0x52, 0x65,
+ 0x73, 0x70, 0x42, 0x25, 0x5a, 0x23, 0x77, 0x69, 0x74, 0x68, 0x69, 0x6e, 0x2e, 0x77, 0x65, 0x62,
+ 0x73, 0x69, 0x74, 0x65, 0x2f, 0x78, 0x2f, 0x63, 0x6d, 0x64, 0x2f, 0x78, 0x65, 0x64, 0x6e, 0x2f,
+ 0x75, 0x70, 0x6c, 0x6f, 0x64, 0x72, 0x2f, 0x70, 0x62, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f,
+ 0x33,
+}
+
+var (
+ file_uplodr_proto_rawDescOnce sync.Once
+ file_uplodr_proto_rawDescData = file_uplodr_proto_rawDesc
+)
+
+func file_uplodr_proto_rawDescGZIP() []byte {
+ file_uplodr_proto_rawDescOnce.Do(func() {
+ file_uplodr_proto_rawDescData = protoimpl.X.CompressGZIP(file_uplodr_proto_rawDescData)
+ })
+ return file_uplodr_proto_rawDescData
+}
+
+var file_uplodr_proto_msgTypes = make([]protoimpl.MessageInfo, 4)
+var file_uplodr_proto_goTypes = []interface{}{
+ (*UploadReq)(nil), // 0: within.website.x.xedn.uplodr.UploadReq
+ (*UploadResp)(nil), // 1: within.website.x.xedn.uplodr.UploadResp
+ (*Variant)(nil), // 2: within.website.x.xedn.uplodr.Variant
+ (*Echo)(nil), // 3: within.website.x.xedn.uplodr.Echo
+}
+var file_uplodr_proto_depIdxs = []int32{
+ 2, // 0: within.website.x.xedn.uplodr.UploadResp.variants:type_name -> within.website.x.xedn.uplodr.Variant
+ 3, // 1: within.website.x.xedn.uplodr.Image.Ping:input_type -> within.website.x.xedn.uplodr.Echo
+ 0, // 2: within.website.x.xedn.uplodr.Image.Upload:input_type -> within.website.x.xedn.uplodr.UploadReq
+ 3, // 3: within.website.x.xedn.uplodr.Image.Ping:output_type -> within.website.x.xedn.uplodr.Echo
+ 1, // 4: within.website.x.xedn.uplodr.Image.Upload:output_type -> within.website.x.xedn.uplodr.UploadResp
+ 3, // [3:5] is the sub-list for method output_type
+ 1, // [1:3] is the sub-list for method input_type
+ 1, // [1:1] is the sub-list for extension type_name
+ 1, // [1:1] is the sub-list for extension extendee
+ 0, // [0:1] is the sub-list for field type_name
+}
+
+func init() { file_uplodr_proto_init() }
+func file_uplodr_proto_init() {
+ if File_uplodr_proto != nil {
+ return
+ }
+ if !protoimpl.UnsafeEnabled {
+ file_uplodr_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} {
+ switch v := v.(*UploadReq); i {
+ case 0:
+ return &v.state
+ case 1:
+ return &v.sizeCache
+ case 2:
+ return &v.unknownFields
+ default:
+ return nil
+ }
+ }
+ file_uplodr_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} {
+ switch v := v.(*UploadResp); i {
+ case 0:
+ return &v.state
+ case 1:
+ return &v.sizeCache
+ case 2:
+ return &v.unknownFields
+ default:
+ return nil
+ }
+ }
+ file_uplodr_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} {
+ switch v := v.(*Variant); i {
+ case 0:
+ return &v.state
+ case 1:
+ return &v.sizeCache
+ case 2:
+ return &v.unknownFields
+ default:
+ return nil
+ }
+ }
+ file_uplodr_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} {
+ switch v := v.(*Echo); i {
+ case 0:
+ return &v.state
+ case 1:
+ return &v.sizeCache
+ case 2:
+ return &v.unknownFields
+ default:
+ return nil
+ }
+ }
+ }
+ type x struct{}
+ out := protoimpl.TypeBuilder{
+ File: protoimpl.DescBuilder{
+ GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
+ RawDescriptor: file_uplodr_proto_rawDesc,
+ NumEnums: 0,
+ NumMessages: 4,
+ NumExtensions: 0,
+ NumServices: 1,
+ },
+ GoTypes: file_uplodr_proto_goTypes,
+ DependencyIndexes: file_uplodr_proto_depIdxs,
+ MessageInfos: file_uplodr_proto_msgTypes,
+ }.Build()
+ File_uplodr_proto = out.File
+ file_uplodr_proto_rawDesc = nil
+ file_uplodr_proto_goTypes = nil
+ file_uplodr_proto_depIdxs = nil
+}
diff --git a/cmd/xedn/uplodr/pb/uplodr_grpc.pb.go b/cmd/xedn/uplodr/pb/uplodr_grpc.pb.go
new file mode 100644
index 0000000..01bc318
--- /dev/null
+++ b/cmd/xedn/uplodr/pb/uplodr_grpc.pb.go
@@ -0,0 +1,146 @@
+// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
+// versions:
+// - protoc-gen-go-grpc v1.3.0
+// - protoc v4.23.4
+// source: uplodr.proto
+
+package pb
+
+import (
+ context "context"
+ grpc "google.golang.org/grpc"
+ codes "google.golang.org/grpc/codes"
+ status "google.golang.org/grpc/status"
+)
+
+// This is a compile-time assertion to ensure that this generated file
+// is compatible with the grpc package it is being compiled against.
+// Requires gRPC-Go v1.32.0 or later.
+const _ = grpc.SupportPackageIsVersion7
+
+const (
+ Image_Ping_FullMethodName = "/within.website.x.xedn.uplodr.Image/Ping"
+ Image_Upload_FullMethodName = "/within.website.x.xedn.uplodr.Image/Upload"
+)
+
+// ImageClient is the client API for Image service.
+//
+// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
+type ImageClient interface {
+ Ping(ctx context.Context, in *Echo, opts ...grpc.CallOption) (*Echo, error)
+ Upload(ctx context.Context, in *UploadReq, opts ...grpc.CallOption) (*UploadResp, error)
+}
+
+type imageClient struct {
+ cc grpc.ClientConnInterface
+}
+
+func NewImageClient(cc grpc.ClientConnInterface) ImageClient {
+ return &imageClient{cc}
+}
+
+func (c *imageClient) Ping(ctx context.Context, in *Echo, opts ...grpc.CallOption) (*Echo, error) {
+ out := new(Echo)
+ err := c.cc.Invoke(ctx, Image_Ping_FullMethodName, in, out, opts...)
+ if err != nil {
+ return nil, err
+ }
+ return out, nil
+}
+
+func (c *imageClient) Upload(ctx context.Context, in *UploadReq, opts ...grpc.CallOption) (*UploadResp, error) {
+ out := new(UploadResp)
+ err := c.cc.Invoke(ctx, Image_Upload_FullMethodName, in, out, opts...)
+ if err != nil {
+ return nil, err
+ }
+ return out, nil
+}
+
+// ImageServer is the server API for Image service.
+// All implementations must embed UnimplementedImageServer
+// for forward compatibility
+type ImageServer interface {
+ Ping(context.Context, *Echo) (*Echo, error)
+ Upload(context.Context, *UploadReq) (*UploadResp, error)
+ mustEmbedUnimplementedImageServer()
+}
+
+// UnimplementedImageServer must be embedded to have forward compatible implementations.
+type UnimplementedImageServer struct {
+}
+
+func (UnimplementedImageServer) Ping(context.Context, *Echo) (*Echo, error) {
+ return nil, status.Errorf(codes.Unimplemented, "method Ping not implemented")
+}
+func (UnimplementedImageServer) Upload(context.Context, *UploadReq) (*UploadResp, error) {
+ return nil, status.Errorf(codes.Unimplemented, "method Upload not implemented")
+}
+func (UnimplementedImageServer) mustEmbedUnimplementedImageServer() {}
+
+// UnsafeImageServer may be embedded to opt out of forward compatibility for this service.
+// Use of this interface is not recommended, as added methods to ImageServer will
+// result in compilation errors.
+type UnsafeImageServer interface {
+ mustEmbedUnimplementedImageServer()
+}
+
+func RegisterImageServer(s grpc.ServiceRegistrar, srv ImageServer) {
+ s.RegisterService(&Image_ServiceDesc, srv)
+}
+
+func _Image_Ping_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
+ in := new(Echo)
+ if err := dec(in); err != nil {
+ return nil, err
+ }
+ if interceptor == nil {
+ return srv.(ImageServer).Ping(ctx, in)
+ }
+ info := &grpc.UnaryServerInfo{
+ Server: srv,
+ FullMethod: Image_Ping_FullMethodName,
+ }
+ handler := func(ctx context.Context, req interface{}) (interface{}, error) {
+ return srv.(ImageServer).Ping(ctx, req.(*Echo))
+ }
+ return interceptor(ctx, in, info, handler)
+}
+
+func _Image_Upload_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
+ in := new(UploadReq)
+ if err := dec(in); err != nil {
+ return nil, err
+ }
+ if interceptor == nil {
+ return srv.(ImageServer).Upload(ctx, in)
+ }
+ info := &grpc.UnaryServerInfo{
+ Server: srv,
+ FullMethod: Image_Upload_FullMethodName,
+ }
+ handler := func(ctx context.Context, req interface{}) (interface{}, error) {
+ return srv.(ImageServer).Upload(ctx, req.(*UploadReq))
+ }
+ return interceptor(ctx, in, info, handler)
+}
+
+// Image_ServiceDesc is the grpc.ServiceDesc for Image service.
+// It's only intended for direct use with grpc.RegisterService,
+// and not to be introspected or modified (even as a copy)
+var Image_ServiceDesc = grpc.ServiceDesc{
+ ServiceName: "within.website.x.xedn.uplodr.Image",
+ HandlerType: (*ImageServer)(nil),
+ Methods: []grpc.MethodDesc{
+ {
+ MethodName: "Ping",
+ Handler: _Image_Ping_Handler,
+ },
+ {
+ MethodName: "Upload",
+ Handler: _Image_Upload_Handler,
+ },
+ },
+ Streams: []grpc.StreamDesc{},
+ Metadata: "uplodr.proto",
+}
diff --git a/cmd/xedn/uplodr/uplodr.proto b/cmd/xedn/uplodr/uplodr.proto
new file mode 100644
index 0000000..f9d4859
--- /dev/null
+++ b/cmd/xedn/uplodr/uplodr.proto
@@ -0,0 +1,27 @@
+syntax = "proto3";
+package within.website.x.xedn.uplodr;
+option go_package = "within.website/x/cmd/xedn/uplodr/pb";
+
+service Image {
+ rpc Ping(Echo) returns (Echo);
+ rpc Upload(UploadReq) returns (UploadResp);
+}
+
+message UploadReq {
+ string file_name = 1;
+ bytes data = 2;
+ string folder = 3;
+}
+
+message UploadResp {
+ repeated Variant variants = 1;
+}
+
+message Variant {
+ string url = 1;
+ string mime_type = 2;
+}
+
+message Echo {
+ string nonce = 1;
+}
diff --git a/internal/tigris.go b/internal/tigris.go
new file mode 100644
index 0000000..91a90e3
--- /dev/null
+++ b/internal/tigris.go
@@ -0,0 +1,21 @@
+package internal
+
+import (
+ "context"
+ "fmt"
+
+ "github.com/aws/aws-sdk-go-v2/aws"
+ awsConfig "github.com/aws/aws-sdk-go-v2/config"
+ "github.com/aws/aws-sdk-go-v2/service/s3"
+)
+
+func TigrisClient(ctx context.Context) (*s3.Client, error) {
+ cfg, err := awsConfig.LoadDefaultConfig(ctx)
+ if err != nil {
+ return nil, fmt.Errorf("failed to load Tigris config: %w", err)
+ }
+
+ return s3.NewFromConfig(cfg, func(o *s3.Options) {
+ o.BaseEndpoint = aws.String("https://fly.storage.tigris.dev")
+ }), nil
+}