mirror of
https://github.com/databasus/databasus.git
synced 2026-04-06 00:32:03 +02:00
FEATURE (azure): Add Azure Blob Storage
This commit is contained in:
5
.github/workflows/ci-release.yml
vendored
5
.github/workflows/ci-release.yml
vendored
@@ -137,6 +137,8 @@ jobs:
|
||||
# testing S3
|
||||
TEST_MINIO_PORT=9000
|
||||
TEST_MINIO_CONSOLE_PORT=9001
|
||||
# testing Azure Blob
|
||||
TEST_AZURITE_BLOB_PORT=10000
|
||||
# testing NAS
|
||||
TEST_NAS_PORT=7006
|
||||
# testing Telegram
|
||||
@@ -165,6 +167,9 @@ jobs:
|
||||
# Wait for MinIO
|
||||
timeout 60 bash -c 'until nc -z localhost 9000; do sleep 2; done'
|
||||
|
||||
# Wait for Azurite
|
||||
timeout 60 bash -c 'until nc -z localhost 10000; do sleep 2; done'
|
||||
|
||||
- name: Create data and temp directories
|
||||
run: |
|
||||
# Create directories that are used for backups and restore
|
||||
|
||||
@@ -31,4 +31,6 @@ TEST_MINIO_CONSOLE_PORT=9001
|
||||
TEST_NAS_PORT=7006
|
||||
# testing Telegram
|
||||
TEST_TELEGRAM_BOT_TOKEN=
|
||||
TEST_TELEGRAM_CHAT_ID=
|
||||
TEST_TELEGRAM_CHAT_ID=
|
||||
# testing Azure Blob Storage
|
||||
TEST_AZURITE_BLOB_PORT=10000
|
||||
@@ -31,6 +31,14 @@ services:
|
||||
container_name: test-minio
|
||||
command: server /data --console-address ":9001"
|
||||
|
||||
# Test Azurite container
|
||||
test-azurite:
|
||||
image: mcr.microsoft.com/azure-storage/azurite
|
||||
ports:
|
||||
- "${TEST_AZURITE_BLOB_PORT:-10000}:10000"
|
||||
container_name: test-azurite
|
||||
command: azurite-blob --blobHost 0.0.0.0
|
||||
|
||||
# Test PostgreSQL containers
|
||||
test-postgres-12:
|
||||
image: postgres:12
|
||||
|
||||
@@ -3,6 +3,8 @@ module postgresus-backend
|
||||
go 1.23.3
|
||||
|
||||
require (
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.20.0
|
||||
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.6.3
|
||||
github.com/gin-contrib/cors v1.7.5
|
||||
github.com/gin-contrib/gzip v1.2.3
|
||||
github.com/gin-gonic/gin v1.10.0
|
||||
@@ -15,16 +17,18 @@ require (
|
||||
github.com/lib/pq v1.10.9
|
||||
github.com/minio/minio-go/v7 v7.0.92
|
||||
github.com/shirou/gopsutil/v4 v4.25.5
|
||||
github.com/stretchr/testify v1.10.0
|
||||
github.com/stretchr/testify v1.11.1
|
||||
github.com/swaggo/files v1.0.1
|
||||
github.com/swaggo/gin-swagger v1.6.0
|
||||
github.com/swaggo/swag v1.16.4
|
||||
golang.org/x/crypto v0.39.0
|
||||
golang.org/x/crypto v0.41.0
|
||||
golang.org/x/time v0.12.0
|
||||
gorm.io/driver/postgres v1.5.11
|
||||
gorm.io/gorm v1.26.1
|
||||
)
|
||||
|
||||
require github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2 // indirect
|
||||
|
||||
require (
|
||||
cloud.google.com/go/auth v0.16.2 // indirect
|
||||
cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect
|
||||
@@ -99,12 +103,12 @@ require (
|
||||
go.opentelemetry.io/otel/metric v1.36.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.36.0 // indirect
|
||||
golang.org/x/arch v0.17.0 // indirect
|
||||
golang.org/x/net v0.41.0 // indirect
|
||||
golang.org/x/net v0.43.0 // indirect
|
||||
golang.org/x/oauth2 v0.30.0
|
||||
golang.org/x/sync v0.15.0 // indirect
|
||||
golang.org/x/sys v0.33.0 // indirect
|
||||
golang.org/x/text v0.26.0 // indirect
|
||||
golang.org/x/tools v0.33.0 // indirect
|
||||
golang.org/x/sync v0.16.0 // indirect
|
||||
golang.org/x/sys v0.35.0 // indirect
|
||||
golang.org/x/text v0.28.0 // indirect
|
||||
golang.org/x/tools v0.35.0 // indirect
|
||||
google.golang.org/api v0.239.0
|
||||
google.golang.org/protobuf v1.36.6 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
|
||||
@@ -6,6 +6,18 @@ cloud.google.com/go/compute/metadata v0.7.0 h1:PBWF+iiAerVNe8UCHxdOt6eHLVc3ydFeO
|
||||
cloud.google.com/go/compute/metadata v0.7.0/go.mod h1:j5MvL9PprKL39t166CoB1uVHfQMs4tFQZZcKwksXUjo=
|
||||
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
|
||||
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.20.0 h1:JXg2dwJUmPB9JmtVmdEB16APJ7jurfbY5jnfXpJoRMc=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.20.0/go.mod h1:YD5h/ldMsG0XiIw7PdyNhLxaM317eFh5yNLccNfGdyw=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.0 h1:KpMC6LFL7mqpExyMC9jVOYRiVhLmamjeZfRsUpB7l4s=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.0/go.mod h1:J7MUC/wtRpfGVbQ5sIItY5/FuVWmvzlY21WAOfQnq/I=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2 h1:9iefClla7iYpfYWdzPCRDozdmndjTm8DXdpCzPajMgA=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2/go.mod h1:XtLgD3ZD34DAaVIIAyG3objl5DynM3CQ/vMcbBNJZGI=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage v1.8.1 h1:/Zt+cDPnpC3OVDm/JKLOs7M2DKmLRIIp3XIx9pHHiig=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage v1.8.1/go.mod h1:Ng3urmn6dYe8gnbCMoHHVl5APYz2txho3koEkV2o2HA=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.6.3 h1:ZJJNFaQ86GVKQ9ehwqyAFE6pIfyicpuJ8IkVaPBc6/4=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.6.3/go.mod h1:URuDvhmATVKqHBH9/0nOiNKk0+YcwfQ3WkK5PqHKxc8=
|
||||
github.com/AzureAD/microsoft-authentication-library-for-go v1.5.0 h1:XkkQbfMyuH2jTSjQjSoihryI8GINRcs4xp8lNawg0FI=
|
||||
github.com/AzureAD/microsoft-authentication-library-for-go v1.5.0/go.mod h1:HKpQxkWaGLJ+D/5H8QRpyQXA1eKjxkFlOMwck5+33Jk=
|
||||
github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
|
||||
github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg=
|
||||
github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
|
||||
@@ -80,6 +92,8 @@ github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
|
||||
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
||||
github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI=
|
||||
github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
|
||||
github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo=
|
||||
github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
|
||||
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
||||
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
@@ -131,6 +145,8 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
|
||||
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
|
||||
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
||||
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
||||
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
|
||||
@@ -159,6 +175,8 @@ github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0
|
||||
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
|
||||
github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c h1:dAMKvw0MlJT1GshSTtih8C2gDs04w8dReiOGXrGLNoY=
|
||||
github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM=
|
||||
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
|
||||
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU=
|
||||
@@ -180,8 +198,8 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
github.com/swaggo/files v1.0.1 h1:J1bVJ4XHZNq0I46UU90611i9/YzdrF7x92oX1ig5IdE=
|
||||
github.com/swaggo/files v1.0.1/go.mod h1:0qXmMNH6sXNf+73t65aKeB+ApmgxdnkQzVTAj2uaMUg=
|
||||
github.com/swaggo/gin-swagger v1.6.0 h1:y8sxvQ3E20/RCyrXeFfg60r6H0Z+SwpTjMYsMm+zy8M=
|
||||
@@ -216,25 +234,25 @@ golang.org/x/arch v0.17.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM=
|
||||
golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U=
|
||||
golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4=
|
||||
golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.25.0 h1:n7a+ZbQKQA/Ysbyb0/6IbB1H/X41mKgbhfv7AfG/44w=
|
||||
golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
|
||||
golang.org/x/mod v0.26.0 h1:EGMPT//Ezu+ylkCijjPc+f4Aih7sZvaAr+O3EHBxvZg=
|
||||
golang.org/x/mod v0.26.0/go.mod h1:/j6NAhSk8iQ723BGAUyoAcn7SlD7s15Dp9Nd/SfeaFQ=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210421230115-4e50805a0758/go.mod h1:72T/g9IO56b78aLF+1Kcs5dz7/ng1VjMUvfKvpfy+jM=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw=
|
||||
golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA=
|
||||
golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE=
|
||||
golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg=
|
||||
golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI=
|
||||
golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8=
|
||||
golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
|
||||
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
@@ -247,8 +265,8 @@ golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBc
|
||||
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
|
||||
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
|
||||
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
@@ -257,15 +275,15 @@ golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
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.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M=
|
||||
golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA=
|
||||
golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
|
||||
golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
|
||||
golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=
|
||||
golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.33.0 h1:4qz2S3zmRxbGIhDIAgjxvFutSvH5EfnsYrRBj0UI0bc=
|
||||
golang.org/x/tools v0.33.0/go.mod h1:CIJMaWEY88juyUfo7UbgPqbC8rU2OqfAV1h2Qp0oMYI=
|
||||
golang.org/x/tools v0.35.0 h1:mBffYraMEf7aa0sB+NuKnuCy8qI/9Bughn8dC2Gu5r0=
|
||||
golang.org/x/tools v0.35.0/go.mod h1:NKdj5HkL/73byiZSJjqJgKn3ep7KjFkBOkR/Hps3VPw=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/api v0.239.0 h1:2hZKUnFZEy81eugPs4e2XzIJ5SOwQg0G82bpXD65Puo=
|
||||
google.golang.org/api v0.239.0/go.mod h1:cOVEm2TpdAGHL2z+UwyS+kmlGr3bVWQQ6sYEqkKje50=
|
||||
|
||||
@@ -44,6 +44,8 @@ type EnvVariables struct {
|
||||
TestMinioPort string `env:"TEST_MINIO_PORT"`
|
||||
TestMinioConsolePort string `env:"TEST_MINIO_CONSOLE_PORT"`
|
||||
|
||||
TestAzuriteBlobPort string `env:"TEST_AZURITE_BLOB_PORT"`
|
||||
|
||||
TestNASPort string `env:"TEST_NAS_PORT"`
|
||||
|
||||
// oauth
|
||||
@@ -184,6 +186,11 @@ func loadEnvVariables() {
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if env.TestAzuriteBlobPort == "" {
|
||||
log.Error("TEST_AZURITE_BLOB_PORT is empty")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if env.TestNASPort == "" {
|
||||
log.Error("TEST_NAS_PORT is empty")
|
||||
os.Exit(1)
|
||||
|
||||
@@ -7,4 +7,5 @@ const (
|
||||
StorageTypeS3 StorageType = "S3"
|
||||
StorageTypeGoogleDrive StorageType = "GOOGLE_DRIVE"
|
||||
StorageTypeNAS StorageType = "NAS"
|
||||
StorageTypeAzureBlob StorageType = "AZURE_BLOB"
|
||||
)
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"errors"
|
||||
"io"
|
||||
"log/slog"
|
||||
azure_blob_storage "postgresus-backend/internal/features/storages/models/azure_blob"
|
||||
google_drive_storage "postgresus-backend/internal/features/storages/models/google_drive"
|
||||
local_storage "postgresus-backend/internal/features/storages/models/local"
|
||||
nas_storage "postgresus-backend/internal/features/storages/models/nas"
|
||||
@@ -24,6 +25,7 @@ type Storage struct {
|
||||
S3Storage *s3_storage.S3Storage `json:"s3Storage" gorm:"foreignKey:StorageID"`
|
||||
GoogleDriveStorage *google_drive_storage.GoogleDriveStorage `json:"googleDriveStorage" gorm:"foreignKey:StorageID"`
|
||||
NASStorage *nas_storage.NASStorage `json:"nasStorage" gorm:"foreignKey:StorageID"`
|
||||
AzureBlobStorage *azure_blob_storage.AzureBlobStorage `json:"azureBlobStorage" gorm:"foreignKey:StorageID"`
|
||||
}
|
||||
|
||||
func (s *Storage) SaveFile(logger *slog.Logger, fileID uuid.UUID, file io.Reader) error {
|
||||
@@ -88,6 +90,10 @@ func (s *Storage) Update(incoming *Storage) {
|
||||
if s.NASStorage != nil && incoming.NASStorage != nil {
|
||||
s.NASStorage.Update(incoming.NASStorage)
|
||||
}
|
||||
case StorageTypeAzureBlob:
|
||||
if s.AzureBlobStorage != nil && incoming.AzureBlobStorage != nil {
|
||||
s.AzureBlobStorage.Update(incoming.AzureBlobStorage)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -101,6 +107,8 @@ func (s *Storage) getSpecificStorage() StorageFileSaver {
|
||||
return s.GoogleDriveStorage
|
||||
case StorageTypeNAS:
|
||||
return s.NASStorage
|
||||
case StorageTypeAzureBlob:
|
||||
return s.AzureBlobStorage
|
||||
default:
|
||||
panic("invalid storage type: " + string(s.Type))
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"postgresus-backend/internal/config"
|
||||
azure_blob_storage "postgresus-backend/internal/features/storages/models/azure_blob"
|
||||
google_drive_storage "postgresus-backend/internal/features/storages/models/google_drive"
|
||||
local_storage "postgresus-backend/internal/features/storages/models/local"
|
||||
nas_storage "postgresus-backend/internal/features/storages/models/nas"
|
||||
@@ -17,6 +18,7 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/Azure/azure-sdk-for-go/sdk/storage/azblob"
|
||||
"github.com/google/uuid"
|
||||
"github.com/minio/minio-go/v7"
|
||||
"github.com/minio/minio-go/v7/pkg/credentials"
|
||||
@@ -32,6 +34,15 @@ type S3Container struct {
|
||||
region string
|
||||
}
|
||||
|
||||
type AzuriteContainer struct {
|
||||
endpoint string
|
||||
accountName string
|
||||
accountKey string
|
||||
containerNameKey string
|
||||
containerNameStr string
|
||||
connectionString string
|
||||
}
|
||||
|
||||
func Test_Storage_BasicOperations(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
@@ -41,6 +52,10 @@ func Test_Storage_BasicOperations(t *testing.T) {
|
||||
s3Container, err := setupS3Container(ctx)
|
||||
require.NoError(t, err, "Failed to setup S3 container")
|
||||
|
||||
// Setup Azurite connection
|
||||
azuriteContainer, err := setupAzuriteContainer(ctx)
|
||||
require.NoError(t, err, "Failed to setup Azurite container")
|
||||
|
||||
// Setup test file
|
||||
testFilePath, err := setupTestFile()
|
||||
require.NoError(t, err, "Failed to setup test file")
|
||||
@@ -88,6 +103,26 @@ func Test_Storage_BasicOperations(t *testing.T) {
|
||||
Path: "test-files",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "AzureBlobStorage_AccountKey",
|
||||
storage: &azure_blob_storage.AzureBlobStorage{
|
||||
StorageID: uuid.New(),
|
||||
AuthMethod: azure_blob_storage.AuthMethodAccountKey,
|
||||
AccountName: azuriteContainer.accountName,
|
||||
AccountKey: azuriteContainer.accountKey,
|
||||
ContainerName: azuriteContainer.containerNameKey,
|
||||
Endpoint: azuriteContainer.endpoint,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "AzureBlobStorage_ConnectionString",
|
||||
storage: &azure_blob_storage.AzureBlobStorage{
|
||||
StorageID: uuid.New(),
|
||||
AuthMethod: azure_blob_storage.AuthMethodConnectionString,
|
||||
ConnectionString: azuriteContainer.connectionString,
|
||||
ContainerName: azuriteContainer.containerNameStr,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// Add Google Drive storage test only if environment variables are available
|
||||
@@ -230,8 +265,59 @@ func setupS3Container(ctx context.Context) (*S3Container, error) {
|
||||
}, nil
|
||||
}
|
||||
|
||||
func setupAzuriteContainer(ctx context.Context) (*AzuriteContainer, error) {
|
||||
env := config.GetEnv()
|
||||
|
||||
accountName := "devstoreaccount1"
|
||||
// this is real testing key for azurite, it's not a real key
|
||||
accountKey := "Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw=="
|
||||
serviceURL := fmt.Sprintf("http://127.0.0.1:%s/%s", env.TestAzuriteBlobPort, accountName)
|
||||
containerNameKey := "test-container-key"
|
||||
containerNameStr := "test-container-connstr"
|
||||
|
||||
// Build explicit connection string for Azurite
|
||||
connectionString := fmt.Sprintf(
|
||||
"DefaultEndpointsProtocol=http;AccountName=%s;AccountKey=%s;BlobEndpoint=http://127.0.0.1:%s/%s",
|
||||
accountName,
|
||||
accountKey,
|
||||
env.TestAzuriteBlobPort,
|
||||
accountName,
|
||||
)
|
||||
|
||||
// Create client using connection string to set up containers
|
||||
client, err := azblob.NewClientFromConnectionString(connectionString, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create azblob client: %w", err)
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(ctx, 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
// Create container for account key auth
|
||||
_, err = client.CreateContainer(ctx, containerNameKey, nil)
|
||||
if err != nil {
|
||||
// Container might already exist, that's okay
|
||||
}
|
||||
|
||||
// Create container for connection string auth
|
||||
_, err = client.CreateContainer(ctx, containerNameStr, nil)
|
||||
if err != nil {
|
||||
// Container might already exist, that's okay
|
||||
}
|
||||
|
||||
return &AzuriteContainer{
|
||||
endpoint: serviceURL,
|
||||
accountName: accountName,
|
||||
accountKey: accountKey,
|
||||
containerNameKey: containerNameKey,
|
||||
containerNameStr: containerNameStr,
|
||||
connectionString: connectionString,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func validateEnvVariables(t *testing.T) {
|
||||
env := config.GetEnv()
|
||||
assert.NotEmpty(t, env.TestMinioPort, "TEST_MINIO_PORT is empty")
|
||||
assert.NotEmpty(t, env.TestAzuriteBlobPort, "TEST_AZURITE_BLOB_PORT is empty")
|
||||
assert.NotEmpty(t, env.TestNASPort, "TEST_NAS_PORT is empty")
|
||||
}
|
||||
|
||||
269
backend/internal/features/storages/models/azure_blob/model.go
Normal file
269
backend/internal/features/storages/models/azure_blob/model.go
Normal file
@@ -0,0 +1,269 @@
|
||||
package azure_blob_storage
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"log/slog"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/Azure/azure-sdk-for-go/sdk/azcore"
|
||||
"github.com/Azure/azure-sdk-for-go/sdk/storage/azblob"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type AuthMethod string
|
||||
|
||||
const (
|
||||
AuthMethodConnectionString AuthMethod = "CONNECTION_STRING"
|
||||
AuthMethodAccountKey AuthMethod = "ACCOUNT_KEY"
|
||||
)
|
||||
|
||||
type AzureBlobStorage struct {
|
||||
StorageID uuid.UUID `json:"storageId" gorm:"primaryKey;type:uuid;column:storage_id"`
|
||||
AuthMethod AuthMethod `json:"authMethod" gorm:"not null;type:text;column:auth_method"`
|
||||
ConnectionString string `json:"connectionString" gorm:"type:text;column:connection_string"`
|
||||
AccountName string `json:"accountName" gorm:"type:text;column:account_name"`
|
||||
AccountKey string `json:"accountKey" gorm:"type:text;column:account_key"`
|
||||
ContainerName string `json:"containerName" gorm:"not null;type:text;column:container_name"`
|
||||
Endpoint string `json:"endpoint" gorm:"type:text;column:endpoint"`
|
||||
Prefix string `json:"prefix" gorm:"type:text;column:prefix"`
|
||||
}
|
||||
|
||||
func (s *AzureBlobStorage) TableName() string {
|
||||
return "azure_blob_storages"
|
||||
}
|
||||
|
||||
func (s *AzureBlobStorage) SaveFile(logger *slog.Logger, fileID uuid.UUID, file io.Reader) error {
|
||||
client, err := s.getClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
blobName := s.buildBlobName(fileID.String())
|
||||
|
||||
_, err = client.UploadStream(
|
||||
context.TODO(),
|
||||
s.ContainerName,
|
||||
blobName,
|
||||
file,
|
||||
nil,
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to upload blob to Azure: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *AzureBlobStorage) GetFile(fileID uuid.UUID) (io.ReadCloser, error) {
|
||||
client, err := s.getClient()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
blobName := s.buildBlobName(fileID.String())
|
||||
|
||||
response, err := client.DownloadStream(
|
||||
context.TODO(),
|
||||
s.ContainerName,
|
||||
blobName,
|
||||
nil,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to download blob from Azure: %w", err)
|
||||
}
|
||||
|
||||
return response.Body, nil
|
||||
}
|
||||
|
||||
func (s *AzureBlobStorage) DeleteFile(fileID uuid.UUID) error {
|
||||
client, err := s.getClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
blobName := s.buildBlobName(fileID.String())
|
||||
|
||||
_, err = client.DeleteBlob(
|
||||
context.TODO(),
|
||||
s.ContainerName,
|
||||
blobName,
|
||||
nil,
|
||||
)
|
||||
if err != nil {
|
||||
var respErr *azcore.ResponseError
|
||||
if errors.As(err, &respErr) && respErr.StatusCode == 404 {
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("failed to delete blob from Azure: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *AzureBlobStorage) Validate() error {
|
||||
if s.ContainerName == "" {
|
||||
return errors.New("container name is required")
|
||||
}
|
||||
|
||||
switch s.AuthMethod {
|
||||
case AuthMethodConnectionString:
|
||||
if s.ConnectionString == "" {
|
||||
return errors.New(
|
||||
"connection string is required when using CONNECTION_STRING auth method",
|
||||
)
|
||||
}
|
||||
case AuthMethodAccountKey:
|
||||
if s.AccountName == "" {
|
||||
return errors.New("account name is required when using ACCOUNT_KEY auth method")
|
||||
}
|
||||
if s.AccountKey == "" {
|
||||
return errors.New("account key is required when using ACCOUNT_KEY auth method")
|
||||
}
|
||||
default:
|
||||
return fmt.Errorf("invalid auth method: %s", s.AuthMethod)
|
||||
}
|
||||
|
||||
_, err := s.getClient()
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid Azure Blob configuration: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *AzureBlobStorage) TestConnection() error {
|
||||
client, err := s.getClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
containerClient := client.ServiceClient().NewContainerClient(s.ContainerName)
|
||||
_, err = containerClient.GetProperties(ctx, nil)
|
||||
if err != nil {
|
||||
var respErr *azcore.ResponseError
|
||||
if errors.As(err, &respErr) {
|
||||
if respErr.StatusCode == 404 {
|
||||
return fmt.Errorf("container '%s' does not exist", s.ContainerName)
|
||||
}
|
||||
}
|
||||
if errors.Is(err, context.DeadlineExceeded) {
|
||||
return errors.New("failed to connect to Azure Blob Storage. Please check params")
|
||||
}
|
||||
return fmt.Errorf("failed to connect to Azure Blob Storage: %w", err)
|
||||
}
|
||||
|
||||
testBlobName := s.buildBlobName(uuid.New().String() + "-test")
|
||||
testData := []byte("test connection")
|
||||
|
||||
_, err = client.UploadStream(
|
||||
ctx,
|
||||
s.ContainerName,
|
||||
testBlobName,
|
||||
bytes.NewReader(testData),
|
||||
nil,
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to upload test blob to Azure: %w", err)
|
||||
}
|
||||
|
||||
_, err = client.DeleteBlob(
|
||||
ctx,
|
||||
s.ContainerName,
|
||||
testBlobName,
|
||||
nil,
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to delete test blob from Azure: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *AzureBlobStorage) HideSensitiveData() {
|
||||
s.ConnectionString = ""
|
||||
s.AccountKey = ""
|
||||
}
|
||||
|
||||
func (s *AzureBlobStorage) Update(incoming *AzureBlobStorage) {
|
||||
s.AuthMethod = incoming.AuthMethod
|
||||
s.ContainerName = incoming.ContainerName
|
||||
s.Endpoint = incoming.Endpoint
|
||||
|
||||
if incoming.ConnectionString != "" {
|
||||
s.ConnectionString = incoming.ConnectionString
|
||||
}
|
||||
|
||||
if incoming.AccountName != "" {
|
||||
s.AccountName = incoming.AccountName
|
||||
}
|
||||
|
||||
if incoming.AccountKey != "" {
|
||||
s.AccountKey = incoming.AccountKey
|
||||
}
|
||||
}
|
||||
|
||||
func (s *AzureBlobStorage) buildBlobName(fileName string) string {
|
||||
if s.Prefix == "" {
|
||||
return fileName
|
||||
}
|
||||
|
||||
prefix := s.Prefix
|
||||
prefix = strings.TrimPrefix(prefix, "/")
|
||||
|
||||
if !strings.HasSuffix(prefix, "/") {
|
||||
prefix = prefix + "/"
|
||||
}
|
||||
|
||||
return prefix + fileName
|
||||
}
|
||||
|
||||
func (s *AzureBlobStorage) getClient() (*azblob.Client, error) {
|
||||
var client *azblob.Client
|
||||
var err error
|
||||
|
||||
switch s.AuthMethod {
|
||||
case AuthMethodConnectionString:
|
||||
client, err = azblob.NewClientFromConnectionString(s.ConnectionString, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf(
|
||||
"failed to create Azure Blob client from connection string: %w",
|
||||
err,
|
||||
)
|
||||
}
|
||||
case AuthMethodAccountKey:
|
||||
accountURL := s.buildAccountURL()
|
||||
credential, credErr := azblob.NewSharedKeyCredential(s.AccountName, s.AccountKey)
|
||||
if credErr != nil {
|
||||
return nil, fmt.Errorf("failed to create Azure shared key credential: %w", credErr)
|
||||
}
|
||||
|
||||
client, err = azblob.NewClientWithSharedKeyCredential(accountURL, credential, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create Azure Blob client with shared key: %w", err)
|
||||
}
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported auth method: %s", s.AuthMethod)
|
||||
}
|
||||
|
||||
return client, nil
|
||||
}
|
||||
|
||||
func (s *AzureBlobStorage) buildAccountURL() string {
|
||||
if s.Endpoint != "" {
|
||||
endpoint := s.Endpoint
|
||||
if !strings.HasPrefix(endpoint, "http://") && !strings.HasPrefix(endpoint, "https://") {
|
||||
endpoint = "https://" + endpoint
|
||||
}
|
||||
return endpoint
|
||||
}
|
||||
|
||||
return fmt.Sprintf("https://%s.blob.core.windows.net/", s.AccountName)
|
||||
}
|
||||
@@ -30,17 +30,21 @@ func (r *StorageRepository) Save(storage *Storage) (*Storage, error) {
|
||||
if storage.NASStorage != nil {
|
||||
storage.NASStorage.StorageID = storage.ID
|
||||
}
|
||||
case StorageTypeAzureBlob:
|
||||
if storage.AzureBlobStorage != nil {
|
||||
storage.AzureBlobStorage.StorageID = storage.ID
|
||||
}
|
||||
}
|
||||
|
||||
if storage.ID == uuid.Nil {
|
||||
if err := tx.Create(storage).
|
||||
Omit("LocalStorage", "S3Storage", "GoogleDriveStorage", "NASStorage").
|
||||
Omit("LocalStorage", "S3Storage", "GoogleDriveStorage", "NASStorage", "AzureBlobStorage").
|
||||
Error; err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
if err := tx.Save(storage).
|
||||
Omit("LocalStorage", "S3Storage", "GoogleDriveStorage", "NASStorage").
|
||||
Omit("LocalStorage", "S3Storage", "GoogleDriveStorage", "NASStorage", "AzureBlobStorage").
|
||||
Error; err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -75,6 +79,13 @@ func (r *StorageRepository) Save(storage *Storage) (*Storage, error) {
|
||||
return err
|
||||
}
|
||||
}
|
||||
case StorageTypeAzureBlob:
|
||||
if storage.AzureBlobStorage != nil {
|
||||
storage.AzureBlobStorage.StorageID = storage.ID // Ensure ID is set
|
||||
if err := tx.Save(storage.AzureBlobStorage).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -96,6 +107,7 @@ func (r *StorageRepository) FindByID(id uuid.UUID) (*Storage, error) {
|
||||
Preload("S3Storage").
|
||||
Preload("GoogleDriveStorage").
|
||||
Preload("NASStorage").
|
||||
Preload("AzureBlobStorage").
|
||||
Where("id = ?", id).
|
||||
First(&s).Error; err != nil {
|
||||
return nil, err
|
||||
@@ -113,6 +125,7 @@ func (r *StorageRepository) FindByWorkspaceID(workspaceID uuid.UUID) ([]*Storage
|
||||
Preload("S3Storage").
|
||||
Preload("GoogleDriveStorage").
|
||||
Preload("NASStorage").
|
||||
Preload("AzureBlobStorage").
|
||||
Where("workspace_id = ?", workspaceID).
|
||||
Order("name ASC").
|
||||
Find(&storages).Error; err != nil {
|
||||
@@ -150,6 +163,12 @@ func (r *StorageRepository) Delete(s *Storage) error {
|
||||
return err
|
||||
}
|
||||
}
|
||||
case StorageTypeAzureBlob:
|
||||
if s.AzureBlobStorage != nil {
|
||||
if err := tx.Delete(s.AzureBlobStorage).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Delete the main storage
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package users_models
|
||||
|
||||
type SecretKey struct {
|
||||
Secret string `gorm:"column:secret"`
|
||||
Secret string `gorm:"column:secret" json:"-"`
|
||||
}
|
||||
|
||||
func (SecretKey) TableName() string {
|
||||
|
||||
28
backend/migrations/20251116195618_add_azure_blob_storage.sql
Normal file
28
backend/migrations/20251116195618_add_azure_blob_storage.sql
Normal file
@@ -0,0 +1,28 @@
|
||||
-- +goose Up
|
||||
-- +goose StatementBegin
|
||||
|
||||
CREATE TABLE azure_blob_storages (
|
||||
storage_id UUID PRIMARY KEY,
|
||||
auth_method TEXT NOT NULL,
|
||||
connection_string TEXT,
|
||||
account_name TEXT,
|
||||
account_key TEXT,
|
||||
container_name TEXT NOT NULL,
|
||||
endpoint TEXT,
|
||||
prefix TEXT
|
||||
);
|
||||
|
||||
ALTER TABLE azure_blob_storages
|
||||
ADD CONSTRAINT fk_azure_blob_storages_storage
|
||||
FOREIGN KEY (storage_id)
|
||||
REFERENCES storages (id)
|
||||
ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED;
|
||||
|
||||
-- +goose StatementEnd
|
||||
|
||||
-- +goose Down
|
||||
-- +goose StatementBegin
|
||||
|
||||
DROP TABLE IF EXISTS azure_blob_storages;
|
||||
|
||||
-- +goose StatementEnd
|
||||
9
frontend/public/icons/storages/azure.svg
Normal file
9
frontend/public/icons/storages/azure.svg
Normal file
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||
<svg width="800px" height="800px" viewBox="0 -28.5 256 256" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<title>path21</title>
|
||||
<g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<path d="M118.431947,187.698037 C151.322003,181.887937 178.48731,177.08008 178.799309,177.013916 L179.366585,176.893612 L148.31513,139.958881 C131.236843,119.644776 117.26369,102.945381 117.26369,102.849118 C117.26369,102.666861 149.32694,14.3716012 149.507189,14.057257 C149.567455,13.952452 171.38747,51.62411 202.400338,105.376064 C231.435152,155.699606 255.372949,197.191547 255.595444,197.580359 L255.999996,198.287301 L157.315912,198.274572 L58.6318456,198.261895 L118.431947,187.698073 L118.431947,187.698037 Z M-4.03864498e-06,176.434723 C-4.03864498e-06,176.382721 14.631291,150.983941 32.5139844,119.992969 L65.0279676,63.6457518 L102.919257,31.8473052 C123.759465,14.3581634 140.866667,0.0274832751 140.935253,0.00062917799 C141.003839,-0.0247829554 140.729691,0.665213042 140.326034,1.53468179 C139.922377,2.40415053 121.407304,42.1170321 99.1814268,89.7855264 L58.7707514,176.455514 L29.3853737,176.492355 C13.2234196,176.512639 -4.03864498e-06,176.486664 -4.03864498e-06,176.434703 L-4.03864498e-06,176.434723 Z" fill="#0089D6" fill-rule="nonzero">
|
||||
|
||||
</path>
|
||||
</g>
|
||||
|
After Width: | Height: | Size: 1.4 KiB |
@@ -7,3 +7,4 @@ export { type NASStorage } from './models/NASStorage';
|
||||
export { getStorageLogoFromType } from './models/getStorageLogoFromType';
|
||||
export { getStorageNameFromType } from './models/getStorageNameFromType';
|
||||
export { type GoogleDriveStorage } from './models/GoogleDriveStorage';
|
||||
export { type AzureBlobStorage } from './models/AzureBlobStorage';
|
||||
|
||||
9
frontend/src/entity/storages/models/AzureBlobStorage.ts
Normal file
9
frontend/src/entity/storages/models/AzureBlobStorage.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
export interface AzureBlobStorage {
|
||||
authMethod: 'CONNECTION_STRING' | 'ACCOUNT_KEY';
|
||||
connectionString: string;
|
||||
accountName: string;
|
||||
accountKey: string;
|
||||
containerName: string;
|
||||
endpoint?: string;
|
||||
prefix?: string;
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { AzureBlobStorage } from './AzureBlobStorage';
|
||||
import type { GoogleDriveStorage } from './GoogleDriveStorage';
|
||||
import type { LocalStorage } from './LocalStorage';
|
||||
import type { NASStorage } from './NASStorage';
|
||||
@@ -16,4 +17,5 @@ export interface Storage {
|
||||
s3Storage?: S3Storage;
|
||||
googleDriveStorage?: GoogleDriveStorage;
|
||||
nasStorage?: NASStorage;
|
||||
azureBlobStorage?: AzureBlobStorage;
|
||||
}
|
||||
|
||||
@@ -3,4 +3,5 @@ export enum StorageType {
|
||||
S3 = 'S3',
|
||||
GOOGLE_DRIVE = 'GOOGLE_DRIVE',
|
||||
NAS = 'NAS',
|
||||
AZURE_BLOB = 'AZURE_BLOB',
|
||||
}
|
||||
|
||||
@@ -10,6 +10,8 @@ export const getStorageLogoFromType = (type: StorageType) => {
|
||||
return '/icons/storages/google-drive.svg';
|
||||
case StorageType.NAS:
|
||||
return '/icons/storages/nas.svg';
|
||||
case StorageType.AZURE_BLOB:
|
||||
return '/icons/storages/azure.svg';
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
|
||||
@@ -10,6 +10,8 @@ export const getStorageNameFromType = (type: StorageType) => {
|
||||
return 'Google Drive';
|
||||
case StorageType.NAS:
|
||||
return 'NAS';
|
||||
case StorageType.AZURE_BLOB:
|
||||
return 'Azure Blob Storage';
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
storageApi,
|
||||
} from '../../../../entity/storages';
|
||||
import { ToastHelper } from '../../../../shared/toast';
|
||||
import { EditAzureBlobStorageComponent } from './storages/EditAzureBlobStorageComponent';
|
||||
import { EditGoogleDriveStorageComponent } from './storages/EditGoogleDriveStorageComponent';
|
||||
import { EditNASStorageComponent } from './storages/EditNASStorageComponent';
|
||||
import { EditS3StorageComponent } from './storages/EditS3StorageComponent';
|
||||
@@ -80,6 +81,7 @@ export function EditStorageComponent({
|
||||
storage.localStorage = undefined;
|
||||
storage.s3Storage = undefined;
|
||||
storage.googleDriveStorage = undefined;
|
||||
storage.azureBlobStorage = undefined;
|
||||
|
||||
if (type === StorageType.LOCAL) {
|
||||
storage.localStorage = {};
|
||||
@@ -115,6 +117,18 @@ export function EditStorageComponent({
|
||||
};
|
||||
}
|
||||
|
||||
if (type === StorageType.AZURE_BLOB) {
|
||||
storage.azureBlobStorage = {
|
||||
authMethod: 'ACCOUNT_KEY',
|
||||
connectionString: '',
|
||||
accountName: '',
|
||||
accountKey: '',
|
||||
containerName: '',
|
||||
endpoint: '',
|
||||
prefix: '',
|
||||
};
|
||||
}
|
||||
|
||||
setStorage(
|
||||
JSON.parse(
|
||||
JSON.stringify({
|
||||
@@ -197,6 +211,26 @@ export function EditStorageComponent({
|
||||
);
|
||||
}
|
||||
|
||||
if (storage.type === StorageType.AZURE_BLOB) {
|
||||
if (storage.id) {
|
||||
return storage.azureBlobStorage?.containerName;
|
||||
}
|
||||
|
||||
const isContainerNameFilled = storage.azureBlobStorage?.containerName;
|
||||
|
||||
if (storage.azureBlobStorage?.authMethod === 'CONNECTION_STRING') {
|
||||
return isContainerNameFilled && storage.azureBlobStorage?.connectionString;
|
||||
}
|
||||
|
||||
if (storage.azureBlobStorage?.authMethod === 'ACCOUNT_KEY') {
|
||||
return (
|
||||
isContainerNameFilled &&
|
||||
storage.azureBlobStorage?.accountName &&
|
||||
storage.azureBlobStorage?.accountKey
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
@@ -231,6 +265,7 @@ export function EditStorageComponent({
|
||||
{ label: 'S3', value: StorageType.S3 },
|
||||
{ label: 'Google Drive', value: StorageType.GOOGLE_DRIVE },
|
||||
{ label: 'NAS', value: StorageType.NAS },
|
||||
{ label: 'Azure Blob Storage', value: StorageType.AZURE_BLOB },
|
||||
]}
|
||||
onChange={(value) => {
|
||||
setStorageType(value);
|
||||
@@ -278,6 +313,17 @@ export function EditStorageComponent({
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{storage?.type === StorageType.AZURE_BLOB && (
|
||||
<EditAzureBlobStorageComponent
|
||||
storage={storage}
|
||||
setStorage={setStorage}
|
||||
setUnsaved={() => {
|
||||
setIsUnsaved(true);
|
||||
setIsTestConnectionSuccess(false);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="mt-3 flex">
|
||||
|
||||
@@ -0,0 +1,226 @@
|
||||
import { DownOutlined, InfoCircleOutlined, UpOutlined } from '@ant-design/icons';
|
||||
import { Input, Radio, Tooltip } from 'antd';
|
||||
import { useState } from 'react';
|
||||
|
||||
import type { Storage } from '../../../../../entity/storages';
|
||||
|
||||
interface Props {
|
||||
storage: Storage;
|
||||
setStorage: (storage: Storage) => void;
|
||||
setUnsaved: () => void;
|
||||
}
|
||||
|
||||
export function EditAzureBlobStorageComponent({ storage, setStorage, setUnsaved }: Props) {
|
||||
const hasAdvancedValues =
|
||||
!!storage?.azureBlobStorage?.prefix || !!storage?.azureBlobStorage?.endpoint;
|
||||
const [showAdvanced, setShowAdvanced] = useState(hasAdvancedValues);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="mb-1 flex items-center">
|
||||
<div className="min-w-[110px]">Auth method</div>
|
||||
<Radio.Group
|
||||
value={storage?.azureBlobStorage?.authMethod || 'ACCOUNT_KEY'}
|
||||
onChange={(e) => {
|
||||
if (!storage?.azureBlobStorage) return;
|
||||
|
||||
setStorage({
|
||||
...storage,
|
||||
azureBlobStorage: {
|
||||
...storage.azureBlobStorage,
|
||||
authMethod: e.target.value,
|
||||
},
|
||||
});
|
||||
setUnsaved();
|
||||
}}
|
||||
size="small"
|
||||
>
|
||||
<Radio value="ACCOUNT_KEY">Account key</Radio>
|
||||
<Radio value="CONNECTION_STRING">Connection string</Radio>
|
||||
</Radio.Group>
|
||||
</div>
|
||||
|
||||
{storage?.azureBlobStorage?.authMethod === 'CONNECTION_STRING' && (
|
||||
<div className="mb-1 flex items-center">
|
||||
<div className="min-w-[110px]">Connection</div>
|
||||
<Input.Password
|
||||
value={storage?.azureBlobStorage?.connectionString || ''}
|
||||
onChange={(e) => {
|
||||
if (!storage?.azureBlobStorage) return;
|
||||
|
||||
setStorage({
|
||||
...storage,
|
||||
azureBlobStorage: {
|
||||
...storage.azureBlobStorage,
|
||||
connectionString: e.target.value.trim(),
|
||||
},
|
||||
});
|
||||
setUnsaved();
|
||||
}}
|
||||
size="small"
|
||||
className="w-full max-w-[250px]"
|
||||
placeholder="DefaultEndpointsProtocol=https;AccountName=..."
|
||||
/>
|
||||
|
||||
<Tooltip
|
||||
className="cursor-pointer"
|
||||
title="Azure Storage connection string from Azure Portal"
|
||||
>
|
||||
<InfoCircleOutlined className="ml-2" style={{ color: 'gray' }} />
|
||||
</Tooltip>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{storage?.azureBlobStorage?.authMethod === 'ACCOUNT_KEY' && (
|
||||
<>
|
||||
<div className="mb-1 flex items-center">
|
||||
<div className="min-w-[110px]">Account name</div>
|
||||
<Input
|
||||
value={storage?.azureBlobStorage?.accountName || ''}
|
||||
onChange={(e) => {
|
||||
if (!storage?.azureBlobStorage) return;
|
||||
|
||||
setStorage({
|
||||
...storage,
|
||||
azureBlobStorage: {
|
||||
...storage.azureBlobStorage,
|
||||
accountName: e.target.value.trim(),
|
||||
},
|
||||
});
|
||||
setUnsaved();
|
||||
}}
|
||||
size="small"
|
||||
className="w-full max-w-[250px]"
|
||||
placeholder="mystorageaccount"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="mb-1 flex items-center">
|
||||
<div className="min-w-[110px]">Account key</div>
|
||||
<Input.Password
|
||||
value={storage?.azureBlobStorage?.accountKey || ''}
|
||||
onChange={(e) => {
|
||||
if (!storage?.azureBlobStorage) return;
|
||||
|
||||
setStorage({
|
||||
...storage,
|
||||
azureBlobStorage: {
|
||||
...storage.azureBlobStorage,
|
||||
accountKey: e.target.value.trim(),
|
||||
},
|
||||
});
|
||||
setUnsaved();
|
||||
}}
|
||||
size="small"
|
||||
className="w-full max-w-[250px]"
|
||||
placeholder="your-account-key"
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
<div className="mb-1 flex items-center">
|
||||
<div className="min-w-[110px]">Container name</div>
|
||||
<Input
|
||||
value={storage?.azureBlobStorage?.containerName || ''}
|
||||
onChange={(e) => {
|
||||
if (!storage?.azureBlobStorage) return;
|
||||
|
||||
setStorage({
|
||||
...storage,
|
||||
azureBlobStorage: {
|
||||
...storage.azureBlobStorage,
|
||||
containerName: e.target.value.trim(),
|
||||
},
|
||||
});
|
||||
setUnsaved();
|
||||
}}
|
||||
size="small"
|
||||
className="w-full max-w-[250px]"
|
||||
placeholder="my-container"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="mt-4 mb-3 flex items-center">
|
||||
<div
|
||||
className="flex cursor-pointer items-center text-sm text-blue-600 hover:text-blue-800"
|
||||
onClick={() => setShowAdvanced(!showAdvanced)}
|
||||
>
|
||||
<span className="mr-2">Advanced settings</span>
|
||||
|
||||
{showAdvanced ? (
|
||||
<UpOutlined style={{ fontSize: '12px' }} />
|
||||
) : (
|
||||
<DownOutlined style={{ fontSize: '12px' }} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{showAdvanced && (
|
||||
<>
|
||||
{storage?.azureBlobStorage?.authMethod === 'ACCOUNT_KEY' && (
|
||||
<div className="mb-1 flex items-center">
|
||||
<div className="min-w-[110px]">Endpoint</div>
|
||||
<Input
|
||||
value={storage?.azureBlobStorage?.endpoint || ''}
|
||||
onChange={(e) => {
|
||||
if (!storage?.azureBlobStorage) return;
|
||||
|
||||
setStorage({
|
||||
...storage,
|
||||
azureBlobStorage: {
|
||||
...storage.azureBlobStorage,
|
||||
endpoint: e.target.value.trim(),
|
||||
},
|
||||
});
|
||||
setUnsaved();
|
||||
}}
|
||||
size="small"
|
||||
className="w-full max-w-[250px]"
|
||||
placeholder="https://myaccount.blob.core.windows.net (optional)"
|
||||
/>
|
||||
|
||||
<Tooltip
|
||||
className="cursor-pointer"
|
||||
title="Custom endpoint URL (optional, leave empty for standard Azure)"
|
||||
>
|
||||
<InfoCircleOutlined className="ml-2" style={{ color: 'gray' }} />
|
||||
</Tooltip>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="mb-1 flex items-center">
|
||||
<div className="min-w-[110px]">Blob prefix</div>
|
||||
<Input
|
||||
value={storage?.azureBlobStorage?.prefix || ''}
|
||||
onChange={(e) => {
|
||||
if (!storage?.azureBlobStorage) return;
|
||||
|
||||
setStorage({
|
||||
...storage,
|
||||
azureBlobStorage: {
|
||||
...storage.azureBlobStorage,
|
||||
prefix: e.target.value.trim(),
|
||||
},
|
||||
});
|
||||
setUnsaved();
|
||||
}}
|
||||
size="small"
|
||||
className="w-full max-w-[250px]"
|
||||
placeholder="my-prefix/ (optional)"
|
||||
/>
|
||||
|
||||
<Tooltip
|
||||
className="cursor-pointer"
|
||||
title="Optional prefix for all blob names (e.g., 'backups/' or 'my_team/')"
|
||||
>
|
||||
<InfoCircleOutlined className="ml-2" style={{ color: 'gray' }} />
|
||||
</Tooltip>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
<div className="mb-5" />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import { type Storage, StorageType } from '../../../../entity/storages';
|
||||
import { getStorageLogoFromType } from '../../../../entity/storages/models/getStorageLogoFromType';
|
||||
import { getStorageNameFromType } from '../../../../entity/storages/models/getStorageNameFromType';
|
||||
import { ShowAzureBlobStorageComponent } from './storages/ShowAzureBlobStorageComponent';
|
||||
import { ShowGoogleDriveStorageComponent } from './storages/ShowGoogleDriveStorageComponent';
|
||||
import { ShowNASStorageComponent } from './storages/ShowNASStorageComponent';
|
||||
import { ShowS3StorageComponent } from './storages/ShowS3StorageComponent';
|
||||
@@ -37,6 +38,12 @@ export function ShowStorageComponent({ storage }: Props) {
|
||||
<div>
|
||||
{storage?.type === StorageType.NAS && <ShowNASStorageComponent storage={storage} />}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
{storage?.type === StorageType.AZURE_BLOB && (
|
||||
<ShowAzureBlobStorageComponent storage={storage} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
import type { Storage } from '../../../../../entity/storages';
|
||||
|
||||
interface Props {
|
||||
storage: Storage;
|
||||
}
|
||||
|
||||
export function ShowAzureBlobStorageComponent({ storage }: Props) {
|
||||
return (
|
||||
<>
|
||||
<div className="mb-1 flex items-center">
|
||||
<div className="min-w-[110px]">Auth method</div>
|
||||
{storage?.azureBlobStorage?.authMethod === 'CONNECTION_STRING'
|
||||
? 'Connection string'
|
||||
: 'Account key'}
|
||||
</div>
|
||||
|
||||
{storage?.azureBlobStorage?.authMethod === 'CONNECTION_STRING' && (
|
||||
<div className="mb-1 flex items-center">
|
||||
<div className="min-w-[110px]">Connection string</div>
|
||||
{'*************'}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{storage?.azureBlobStorage?.authMethod === 'ACCOUNT_KEY' && (
|
||||
<>
|
||||
<div className="mb-1 flex items-center">
|
||||
<div className="min-w-[110px]">Account name</div>
|
||||
{storage?.azureBlobStorage?.accountName || '-'}
|
||||
</div>
|
||||
|
||||
<div className="mb-1 flex items-center">
|
||||
<div className="min-w-[110px]">Account key</div>
|
||||
{'*************'}
|
||||
</div>
|
||||
|
||||
<div className="mb-1 flex items-center">
|
||||
<div className="min-w-[110px]">Endpoint</div>
|
||||
{storage?.azureBlobStorage?.endpoint || '-'}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
<div className="mb-1 flex items-center">
|
||||
<div className="min-w-[110px]">Container name</div>
|
||||
{storage?.azureBlobStorage?.containerName || '-'}
|
||||
</div>
|
||||
|
||||
{storage?.azureBlobStorage?.prefix && (
|
||||
<div className="mb-1 flex items-center">
|
||||
<div className="min-w-[110px]">Prefix</div>
|
||||
{storage.azureBlobStorage.prefix}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user