mirror of
https://github.com/databasus/databasus.git
synced 2026-04-06 00:32:03 +02:00
Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
09fc893979 | ||
|
|
8c8c712d97 | ||
|
|
7a44d0150f | ||
|
|
4e1cee2aa2 | ||
|
|
5d685e0a39 | ||
|
|
18b8178608 | ||
|
|
981d560768 | ||
|
|
02d9cda86f | ||
|
|
cefedb6ddd | ||
|
|
27d891fb34 |
@@ -312,8 +312,6 @@ if [ "\$CURRENT_UID" != "\$PUID" ]; then
|
|||||||
usermod -o -u "\$PUID" postgres
|
usermod -o -u "\$PUID" postgres
|
||||||
fi
|
fi
|
||||||
|
|
||||||
chown -R postgres:postgres /var/run/postgresql
|
|
||||||
|
|
||||||
# PostgreSQL 17 binary paths
|
# PostgreSQL 17 binary paths
|
||||||
PG_BIN="/usr/lib/postgresql/17/bin"
|
PG_BIN="/usr/lib/postgresql/17/bin"
|
||||||
|
|
||||||
@@ -426,7 +424,12 @@ fi
|
|||||||
# Function to start PostgreSQL and wait for it to be ready
|
# Function to start PostgreSQL and wait for it to be ready
|
||||||
start_postgres() {
|
start_postgres() {
|
||||||
echo "Starting PostgreSQL..."
|
echo "Starting PostgreSQL..."
|
||||||
gosu postgres \$PG_BIN/postgres -D /databasus-data/pgdata -p 5437 &
|
# -k /tmp: create Unix socket and lock file in /tmp instead of /var/run/postgresql/.
|
||||||
|
# On NAS systems (e.g. TrueNAS Scale), the ZFS-backed Docker overlay filesystem
|
||||||
|
# ignores chown/chmod on directories from image layers, so PostgreSQL gets
|
||||||
|
# "Permission denied" when creating .s.PGSQL.5437.lock in /var/run/postgresql/.
|
||||||
|
# All internal connections use TCP (-h localhost), so the socket location does not matter.
|
||||||
|
gosu postgres \$PG_BIN/postgres -D /databasus-data/pgdata -p 5437 -k /tmp &
|
||||||
POSTGRES_PID=\$!
|
POSTGRES_PID=\$!
|
||||||
|
|
||||||
echo "Waiting for PostgreSQL to be ready..."
|
echo "Waiting for PostgreSQL to be ready..."
|
||||||
|
|||||||
@@ -280,7 +280,6 @@ func (n *BackuperNode) MakeBackup(backupID uuid.UUID, isCallNotifier bool) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
backup.Status = backups_core.BackupStatusCompleted
|
|
||||||
backup.BackupDurationMs = time.Since(start).Milliseconds()
|
backup.BackupDurationMs = time.Since(start).Milliseconds()
|
||||||
|
|
||||||
// Update backup with encryption metadata if provided
|
// Update backup with encryption metadata if provided
|
||||||
@@ -297,12 +296,6 @@ func (n *BackuperNode) MakeBackup(backupID uuid.UUID, isCallNotifier bool) {
|
|||||||
backup.Encryption = backupMetadata.Encryption
|
backup.Encryption = backupMetadata.Encryption
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := n.backupRepository.Save(backup); err != nil {
|
|
||||||
n.logger.Error("Failed to save backup", "error", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Save metadata file to storage
|
|
||||||
if backupMetadata != nil {
|
if backupMetadata != nil {
|
||||||
metadataJSON, err := json.Marshal(backupMetadata)
|
metadataJSON, err := json.Marshal(backupMetadata)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -335,6 +328,13 @@ func (n *BackuperNode) MakeBackup(backupID uuid.UUID, isCallNotifier bool) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
backup.Status = backups_core.BackupStatusCompleted
|
||||||
|
|
||||||
|
if err := n.backupRepository.Save(backup); err != nil {
|
||||||
|
n.logger.Error("Failed to save backup", "error", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// Update database last backup time
|
// Update database last backup time
|
||||||
now := time.Now().UTC()
|
now := time.Now().UTC()
|
||||||
if updateErr := n.databaseService.SetLastBackupTime(databaseID, now); updateErr != nil {
|
if updateErr := n.databaseService.SetLastBackupTime(databaseID, now); updateErr != nil {
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import (
|
|||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
"databasus-backend/internal/config"
|
"databasus-backend/internal/config"
|
||||||
audit_logs "databasus-backend/internal/features/audit_logs"
|
audit_logs "databasus-backend/internal/features/audit_logs"
|
||||||
@@ -1516,14 +1517,14 @@ func Test_MakeBackup_VerifyBackupAndMetadataFilesExistInStorage(t *testing.T) {
|
|||||||
encryptor := encryption.GetFieldEncryptor()
|
encryptor := encryption.GetFieldEncryptor()
|
||||||
|
|
||||||
backupFile, err := backupStorage.GetFile(encryptor, backup.FileName)
|
backupFile, err := backupStorage.GetFile(encryptor, backup.FileName)
|
||||||
assert.NoError(t, err)
|
require.NoError(t, err)
|
||||||
backupFile.Close()
|
backupFile.Close()
|
||||||
|
|
||||||
metadataFile, err := backupStorage.GetFile(encryptor, backup.FileName+".metadata")
|
metadataFile, err := backupStorage.GetFile(encryptor, backup.FileName+".metadata")
|
||||||
assert.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
metadataContent, err := io.ReadAll(metadataFile)
|
metadataContent, err := io.ReadAll(metadataFile)
|
||||||
assert.NoError(t, err)
|
require.NoError(t, err)
|
||||||
metadataFile.Close()
|
metadataFile.Close()
|
||||||
|
|
||||||
var storageMetadata backups_common.BackupMetadata
|
var storageMetadata backups_common.BackupMetadata
|
||||||
|
|||||||
@@ -70,6 +70,14 @@ func (uc *RestoreMariadbBackupUsecase) Execute(
|
|||||||
"--verbose",
|
"--verbose",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Disable Galera Cluster replication for the restore session to prevent
|
||||||
|
// "Maximum writeset size exceeded" errors on large restores.
|
||||||
|
// wsrep_on is available in MariaDB 10.1+ (all builds with Galera support).
|
||||||
|
// On non-Galera instances the variable still exists but is a no-op.
|
||||||
|
if mdb.Version != tools.MariadbVersion55 {
|
||||||
|
args = append(args, "--init-command=SET SESSION wsrep_on=OFF")
|
||||||
|
}
|
||||||
|
|
||||||
if !config.GetEnv().IsCloud {
|
if !config.GetEnv().IsCloud {
|
||||||
args = append(args, "--max-allowed-packet=1G")
|
args = append(args, "--max-allowed-packet=1G")
|
||||||
}
|
}
|
||||||
@@ -379,6 +387,13 @@ func (uc *RestoreMariadbBackupUsecase) handleMariadbRestoreError(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if containsIgnoreCase(stderrStr, "writeset size exceeded") {
|
||||||
|
return fmt.Errorf(
|
||||||
|
"MariaDB Galera Cluster writeset size limit exceeded. Try increasing wsrep_max_ws_size on your cluster nodes. stderr: %s",
|
||||||
|
stderrStr,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
return errors.New(errorMsg)
|
return errors.New(errorMsg)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -110,6 +110,18 @@ func Test_Storage_BasicOperations(t *testing.T) {
|
|||||||
S3Endpoint: "http://" + s3Container.endpoint,
|
S3Endpoint: "http://" + s3Container.endpoint,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "S3Storage_WithStorageClass",
|
||||||
|
storage: &s3_storage.S3Storage{
|
||||||
|
StorageID: uuid.New(),
|
||||||
|
S3Bucket: s3Container.bucketName,
|
||||||
|
S3Region: s3Container.region,
|
||||||
|
S3AccessKey: s3Container.accessKey,
|
||||||
|
S3SecretKey: s3Container.secretKey,
|
||||||
|
S3Endpoint: "http://" + s3Container.endpoint,
|
||||||
|
S3StorageClass: s3_storage.S3StorageClassStandard,
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "NASStorage",
|
name: "NASStorage",
|
||||||
storage: &nas_storage.NASStorage{
|
storage: &nas_storage.NASStorage{
|
||||||
|
|||||||
13
backend/internal/features/storages/models/s3/enums.go
Normal file
13
backend/internal/features/storages/models/s3/enums.go
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
package s3_storage
|
||||||
|
|
||||||
|
type S3StorageClass string
|
||||||
|
|
||||||
|
const (
|
||||||
|
S3StorageClassDefault S3StorageClass = ""
|
||||||
|
S3StorageClassStandard S3StorageClass = "STANDARD"
|
||||||
|
S3StorageClassStandardIA S3StorageClass = "STANDARD_IA"
|
||||||
|
S3StorageClassOnezoneIA S3StorageClass = "ONEZONE_IA"
|
||||||
|
S3StorageClassIntelligentTiering S3StorageClass = "INTELLIGENT_TIERING"
|
||||||
|
S3StorageClassReducedRedundancy S3StorageClass = "REDUCED_REDUNDANCY"
|
||||||
|
S3StorageClassGlacierIR S3StorageClass = "GLACIER_IR"
|
||||||
|
)
|
||||||
@@ -43,9 +43,10 @@ type S3Storage struct {
|
|||||||
S3SecretKey string `json:"s3SecretKey" gorm:"not null;type:text;column:s3_secret_key"`
|
S3SecretKey string `json:"s3SecretKey" gorm:"not null;type:text;column:s3_secret_key"`
|
||||||
S3Endpoint string `json:"s3Endpoint" gorm:"type:text;column:s3_endpoint"`
|
S3Endpoint string `json:"s3Endpoint" gorm:"type:text;column:s3_endpoint"`
|
||||||
|
|
||||||
S3Prefix string `json:"s3Prefix" gorm:"type:text;column:s3_prefix"`
|
S3Prefix string `json:"s3Prefix" gorm:"type:text;column:s3_prefix"`
|
||||||
S3UseVirtualHostedStyle bool `json:"s3UseVirtualHostedStyle" gorm:"default:false;column:s3_use_virtual_hosted_style"`
|
S3UseVirtualHostedStyle bool `json:"s3UseVirtualHostedStyle" gorm:"default:false;column:s3_use_virtual_hosted_style"`
|
||||||
SkipTLSVerify bool `json:"skipTLSVerify" gorm:"default:false;column:skip_tls_verify"`
|
SkipTLSVerify bool `json:"skipTLSVerify" gorm:"default:false;column:skip_tls_verify"`
|
||||||
|
S3StorageClass S3StorageClass `json:"s3StorageClass" gorm:"type:text;column:s3_storage_class;default:''"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *S3Storage) TableName() string {
|
func (s *S3Storage) TableName() string {
|
||||||
@@ -76,7 +77,7 @@ func (s *S3Storage) SaveFile(
|
|||||||
ctx,
|
ctx,
|
||||||
s.S3Bucket,
|
s.S3Bucket,
|
||||||
objectKey,
|
objectKey,
|
||||||
minio.PutObjectOptions{},
|
s.putObjectOptions(),
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to initiate multipart upload: %w", err)
|
return fmt.Errorf("failed to initiate multipart upload: %w", err)
|
||||||
@@ -151,15 +152,16 @@ func (s *S3Storage) SaveFile(
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
opts := s.putObjectOptions()
|
||||||
|
opts.SendContentMd5 = true
|
||||||
|
|
||||||
_, err = client.PutObject(
|
_, err = client.PutObject(
|
||||||
ctx,
|
ctx,
|
||||||
s.S3Bucket,
|
s.S3Bucket,
|
||||||
objectKey,
|
objectKey,
|
||||||
bytes.NewReader([]byte{}),
|
bytes.NewReader([]byte{}),
|
||||||
0,
|
0,
|
||||||
minio.PutObjectOptions{
|
opts,
|
||||||
SendContentMd5: true,
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to upload empty file: %w", err)
|
return fmt.Errorf("failed to upload empty file: %w", err)
|
||||||
@@ -173,7 +175,7 @@ func (s *S3Storage) SaveFile(
|
|||||||
objectKey,
|
objectKey,
|
||||||
uploadID,
|
uploadID,
|
||||||
parts,
|
parts,
|
||||||
minio.PutObjectOptions{},
|
s.putObjectOptions(),
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
_ = coreClient.AbortMultipartUpload(ctx, s.S3Bucket, objectKey, uploadID)
|
_ = coreClient.AbortMultipartUpload(ctx, s.S3Bucket, objectKey, uploadID)
|
||||||
@@ -350,6 +352,7 @@ func (s *S3Storage) Update(incoming *S3Storage) {
|
|||||||
s.S3Endpoint = incoming.S3Endpoint
|
s.S3Endpoint = incoming.S3Endpoint
|
||||||
s.S3UseVirtualHostedStyle = incoming.S3UseVirtualHostedStyle
|
s.S3UseVirtualHostedStyle = incoming.S3UseVirtualHostedStyle
|
||||||
s.SkipTLSVerify = incoming.SkipTLSVerify
|
s.SkipTLSVerify = incoming.SkipTLSVerify
|
||||||
|
s.S3StorageClass = incoming.S3StorageClass
|
||||||
|
|
||||||
if incoming.S3AccessKey != "" {
|
if incoming.S3AccessKey != "" {
|
||||||
s.S3AccessKey = incoming.S3AccessKey
|
s.S3AccessKey = incoming.S3AccessKey
|
||||||
@@ -363,6 +366,12 @@ func (s *S3Storage) Update(incoming *S3Storage) {
|
|||||||
// otherwise we will have to transfer all the data to the new prefix
|
// otherwise we will have to transfer all the data to the new prefix
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *S3Storage) putObjectOptions() minio.PutObjectOptions {
|
||||||
|
return minio.PutObjectOptions{
|
||||||
|
StorageClass: string(s.S3StorageClass),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (s *S3Storage) buildObjectKey(fileName string) string {
|
func (s *S3Storage) buildObjectKey(fileName string) string {
|
||||||
if s.S3Prefix == "" {
|
if s.S3Prefix == "" {
|
||||||
return fileName
|
return fileName
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
-- +goose Up
|
||||||
|
-- +goose StatementBegin
|
||||||
|
ALTER TABLE s3_storages
|
||||||
|
ADD COLUMN s3_storage_class TEXT NOT NULL DEFAULT '';
|
||||||
|
-- +goose StatementEnd
|
||||||
|
|
||||||
|
-- +goose Down
|
||||||
|
-- +goose StatementBegin
|
||||||
|
ALTER TABLE s3_storages
|
||||||
|
DROP COLUMN s3_storage_class;
|
||||||
|
-- +goose StatementEnd
|
||||||
@@ -3,6 +3,7 @@ export { type Storage } from './models/Storage';
|
|||||||
export { StorageType } from './models/StorageType';
|
export { StorageType } from './models/StorageType';
|
||||||
export { type LocalStorage } from './models/LocalStorage';
|
export { type LocalStorage } from './models/LocalStorage';
|
||||||
export { type S3Storage } from './models/S3Storage';
|
export { type S3Storage } from './models/S3Storage';
|
||||||
|
export { S3StorageClass, S3StorageClassLabels } from './models/S3StorageClass';
|
||||||
export { type NASStorage } from './models/NASStorage';
|
export { type NASStorage } from './models/NASStorage';
|
||||||
export { getStorageLogoFromType } from './models/getStorageLogoFromType';
|
export { getStorageLogoFromType } from './models/getStorageLogoFromType';
|
||||||
export { getStorageNameFromType } from './models/getStorageNameFromType';
|
export { getStorageNameFromType } from './models/getStorageNameFromType';
|
||||||
|
|||||||
@@ -7,4 +7,5 @@ export interface S3Storage {
|
|||||||
s3Prefix?: string;
|
s3Prefix?: string;
|
||||||
s3UseVirtualHostedStyle?: boolean;
|
s3UseVirtualHostedStyle?: boolean;
|
||||||
skipTLSVerify?: boolean;
|
skipTLSVerify?: boolean;
|
||||||
|
s3StorageClass?: string;
|
||||||
}
|
}
|
||||||
|
|||||||
19
frontend/src/entity/storages/models/S3StorageClass.ts
Normal file
19
frontend/src/entity/storages/models/S3StorageClass.ts
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
export enum S3StorageClass {
|
||||||
|
DEFAULT = '',
|
||||||
|
STANDARD = 'STANDARD',
|
||||||
|
STANDARD_IA = 'STANDARD_IA',
|
||||||
|
ONEZONE_IA = 'ONEZONE_IA',
|
||||||
|
INTELLIGENT_TIERING = 'INTELLIGENT_TIERING',
|
||||||
|
REDUCED_REDUNDANCY = 'REDUCED_REDUNDANCY',
|
||||||
|
GLACIER_IR = 'GLACIER_IR',
|
||||||
|
}
|
||||||
|
|
||||||
|
export const S3StorageClassLabels: Record<S3StorageClass, string> = {
|
||||||
|
[S3StorageClass.DEFAULT]: 'Default (Standard)',
|
||||||
|
[S3StorageClass.STANDARD]: 'Standard',
|
||||||
|
[S3StorageClass.STANDARD_IA]: 'Standard - Infrequent Access',
|
||||||
|
[S3StorageClass.ONEZONE_IA]: 'One Zone - Infrequent Access',
|
||||||
|
[S3StorageClass.INTELLIGENT_TIERING]: 'Intelligent Tiering',
|
||||||
|
[S3StorageClass.REDUCED_REDUNDANCY]: 'Reduced Redundancy',
|
||||||
|
[S3StorageClass.GLACIER_IR]: 'Glacier Instant Retrieval',
|
||||||
|
};
|
||||||
@@ -558,7 +558,7 @@ export const BackupsComponent = ({
|
|||||||
<div className="mt-5" />
|
<div className="mt-5" />
|
||||||
|
|
||||||
{database.postgresql?.backupType !== PostgresBackupType.WAL_V1 && (
|
{database.postgresql?.backupType !== PostgresBackupType.WAL_V1 && (
|
||||||
<div className="flex">
|
<div className="flex items-center">
|
||||||
<Button
|
<Button
|
||||||
onClick={makeBackup}
|
onClick={makeBackup}
|
||||||
className="mr-1"
|
className="mr-1"
|
||||||
@@ -569,6 +569,17 @@ export const BackupsComponent = ({
|
|||||||
<span className="md:hidden">Backup now</span>
|
<span className="md:hidden">Backup now</span>
|
||||||
<span className="hidden md:inline">Make backup right now</span>
|
<span className="hidden md:inline">Make backup right now</span>
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
|
{!IS_CLOUD && (
|
||||||
|
<a
|
||||||
|
href="https://databasus.com/cloud"
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
className="inline-flex h-8 items-center rounded-md border border-blue-600 px-[15px] text-sm leading-none transition-colors hover:bg-blue-50 dark:!text-white dark:hover:bg-blue-900/30"
|
||||||
|
>
|
||||||
|
Get 24x7 uptime and 2x independent backups copy
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
|
import { ExclamationCircleOutlined } from '@ant-design/icons';
|
||||||
import { Button, Modal, Spin } from 'antd';
|
import { Button, Modal, Spin } from 'antd';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
import { IS_CLOUD } from '../../../constants';
|
||||||
import { storageApi } from '../../../entity/storages';
|
import { storageApi } from '../../../entity/storages';
|
||||||
import type { Storage } from '../../../entity/storages';
|
import type { Storage } from '../../../entity/storages';
|
||||||
import type { UserProfile } from '../../../entity/users';
|
import type { UserProfile } from '../../../entity/users';
|
||||||
@@ -100,6 +102,31 @@ export const StoragesComponent = ({
|
|||||||
>
|
>
|
||||||
{storages.length >= 5 && isCanManageStorages && addStorageButton}
|
{storages.length >= 5 && isCanManageStorages && addStorageButton}
|
||||||
|
|
||||||
|
{!IS_CLOUD && (
|
||||||
|
<div className="mb-3 rounded bg-yellow-50 p-3 shadow dark:bg-yellow-900/30">
|
||||||
|
<div className="mb-1 flex items-center gap-1.5 text-sm font-bold text-yellow-700 dark:text-yellow-400">
|
||||||
|
<ExclamationCircleOutlined />
|
||||||
|
Self-hosted notice
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="text-sm !text-yellow-600 dark:!text-yellow-500">
|
||||||
|
Do not forget to backup the storage itself as it contains all your backups.
|
||||||
|
<br /> Or you can use cloud{"'"}s build-in{' '}
|
||||||
|
<u>unlimited storage with double reservation</u>. We care about security,
|
||||||
|
maintainance and 24x7 uptime
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<a
|
||||||
|
href="https://databasus.com/cloud"
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
className="mt-2 block w-full rounded-md !bg-green-600 px-4 py-1.5 text-center text-sm font-medium !text-white transition-colors hover:!bg-green-700 dark:!bg-green-700 dark:hover:!bg-green-800"
|
||||||
|
>
|
||||||
|
Use cloud storage from $9
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{storages.map((storage) => (
|
{storages.map((storage) => (
|
||||||
<StorageCardComponent
|
<StorageCardComponent
|
||||||
key={storage.id}
|
key={storage.id}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { InfoCircleOutlined } from '@ant-design/icons';
|
import { ExclamationCircleOutlined, InfoCircleOutlined } from '@ant-design/icons';
|
||||||
import { Button, Input, Select, Switch, Tooltip } from 'antd';
|
import { Button, Input, Select, Switch, Tooltip } from 'antd';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
@@ -14,6 +14,7 @@ import { ToastHelper } from '../../../../shared/toast';
|
|||||||
import { EditAzureBlobStorageComponent } from './storages/EditAzureBlobStorageComponent';
|
import { EditAzureBlobStorageComponent } from './storages/EditAzureBlobStorageComponent';
|
||||||
import { EditFTPStorageComponent } from './storages/EditFTPStorageComponent';
|
import { EditFTPStorageComponent } from './storages/EditFTPStorageComponent';
|
||||||
import { EditGoogleDriveStorageComponent } from './storages/EditGoogleDriveStorageComponent';
|
import { EditGoogleDriveStorageComponent } from './storages/EditGoogleDriveStorageComponent';
|
||||||
|
import { EditLocalStorageComponent } from './storages/EditLocalStorageComponent';
|
||||||
import { EditNASStorageComponent } from './storages/EditNASStorageComponent';
|
import { EditNASStorageComponent } from './storages/EditNASStorageComponent';
|
||||||
import { EditRcloneStorageComponent } from './storages/EditRcloneStorageComponent';
|
import { EditRcloneStorageComponent } from './storages/EditRcloneStorageComponent';
|
||||||
import { EditS3StorageComponent } from './storages/EditS3StorageComponent';
|
import { EditS3StorageComponent } from './storages/EditS3StorageComponent';
|
||||||
@@ -486,6 +487,35 @@ export function EditStorageComponent({
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{storage?.type === StorageType.LOCAL && <EditLocalStorageComponent />}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
{!IS_CLOUD && (
|
||||||
|
<div className="mb-3 rounded bg-yellow-50 p-3 shadow dark:bg-yellow-900/30">
|
||||||
|
<div className="mb-1 flex items-center gap-1.5 text-sm font-bold text-yellow-700 dark:text-yellow-400">
|
||||||
|
<ExclamationCircleOutlined />
|
||||||
|
Self-hosted notice
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="text-sm !text-yellow-600 dark:!text-yellow-500">
|
||||||
|
Do not forget to backup the storage itself as it contains all your backups.
|
||||||
|
<br /> Or you can use cloud{"'"}s build-in{' '}
|
||||||
|
<u>unlimited storage with double reservation</u>. We care about security, maintainance
|
||||||
|
and 24x7 uptime for you
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<a
|
||||||
|
href="https://databasus.com/cloud"
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
className="mt-2 block w-full rounded-md !bg-green-600 px-4 py-1.5 text-center text-sm font-medium !text-white transition-colors hover:!bg-green-700 dark:!bg-green-700 dark:hover:!bg-green-800"
|
||||||
|
>
|
||||||
|
Use cloud storage from $9
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mt-3 flex">
|
<div className="mt-3 flex">
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import { ExclamationCircleOutlined } from '@ant-design/icons';
|
||||||
|
|
||||||
|
export function EditLocalStorageComponent() {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="max-w-[360px] text-yellow-600 dark:text-yellow-400">
|
||||||
|
<ExclamationCircleOutlined /> Be careful: with local storage you may run out of ROM memory.
|
||||||
|
It is recommended to use S3 or unlimited storages
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mb-5" />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
import { DownOutlined, InfoCircleOutlined, UpOutlined } from '@ant-design/icons';
|
import { DownOutlined, InfoCircleOutlined, UpOutlined } from '@ant-design/icons';
|
||||||
import { Checkbox, Input, Tooltip } from 'antd';
|
import { Checkbox, Input, Select, Tooltip } from 'antd';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
import type { Storage } from '../../../../../entity/storages';
|
import { S3StorageClass, S3StorageClassLabels, type Storage } from '../../../../../entity/storages';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
storage: Storage;
|
storage: Storage;
|
||||||
@@ -20,7 +20,8 @@ export function EditS3StorageComponent({
|
|||||||
const hasAdvancedValues =
|
const hasAdvancedValues =
|
||||||
!!storage?.s3Storage?.s3Prefix ||
|
!!storage?.s3Storage?.s3Prefix ||
|
||||||
!!storage?.s3Storage?.s3UseVirtualHostedStyle ||
|
!!storage?.s3Storage?.s3UseVirtualHostedStyle ||
|
||||||
!!storage?.s3Storage?.skipTLSVerify;
|
!!storage?.s3Storage?.skipTLSVerify ||
|
||||||
|
!!storage?.s3Storage?.s3StorageClass;
|
||||||
const [showAdvanced, setShowAdvanced] = useState(hasAdvancedValues);
|
const [showAdvanced, setShowAdvanced] = useState(hasAdvancedValues);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -278,6 +279,40 @@ export function EditS3StorageComponent({
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="mb-1 flex w-full flex-col items-start sm:flex-row sm:items-center">
|
||||||
|
<div className="mb-1 min-w-[110px] sm:mb-0">Storage class</div>
|
||||||
|
<div className="flex items-center">
|
||||||
|
<Select
|
||||||
|
value={storage?.s3Storage?.s3StorageClass || S3StorageClass.DEFAULT}
|
||||||
|
options={Object.entries(S3StorageClassLabels).map(([value, label]) => ({
|
||||||
|
value,
|
||||||
|
label,
|
||||||
|
}))}
|
||||||
|
onChange={(value) => {
|
||||||
|
if (!storage?.s3Storage) return;
|
||||||
|
|
||||||
|
setStorage({
|
||||||
|
...storage,
|
||||||
|
s3Storage: {
|
||||||
|
...storage.s3Storage,
|
||||||
|
s3StorageClass: value,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
setUnsaved();
|
||||||
|
}}
|
||||||
|
size="small"
|
||||||
|
className="w-[250px] max-w-[250px]"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Tooltip
|
||||||
|
className="cursor-pointer"
|
||||||
|
title="S3 storage class for uploaded objects. Leave as default for Standard. Some providers offer cheaper classes like One Zone IA. Do not use Glacier/Deep Archive — files must be immediately accessible for restores."
|
||||||
|
>
|
||||||
|
<InfoCircleOutlined className="ml-2" style={{ color: 'gray' }} />
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { Storage } from '../../../../../entity/storages';
|
import { S3StorageClass, S3StorageClassLabels, type Storage } from '../../../../../entity/storages';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
storage: Storage;
|
storage: Storage;
|
||||||
@@ -52,6 +52,14 @@ export function ShowS3StorageComponent({ storage }: Props) {
|
|||||||
Enabled
|
Enabled
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{storage?.s3Storage?.s3StorageClass && (
|
||||||
|
<div className="mb-1 flex items-center">
|
||||||
|
<div className="min-w-[110px]">Storage Class</div>
|
||||||
|
{S3StorageClassLabels[storage.s3Storage.s3StorageClass as S3StorageClass] ||
|
||||||
|
storage.s3Storage.s3StorageClass}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,12 +34,12 @@ export function AuthNavbarComponent() {
|
|||||||
|
|
||||||
{!IS_CLOUD && (
|
{!IS_CLOUD && (
|
||||||
<a
|
<a
|
||||||
className="!text-black !underline !decoration-blue-600 !decoration-2 underline-offset-2 hover:opacity-80 dark:!text-gray-200"
|
className="!text-black hover:opacity-80 dark:!text-gray-200"
|
||||||
href="https://databasus.com/cloud"
|
href="https://databasus.com/cloud"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noreferrer"
|
rel="noreferrer"
|
||||||
>
|
>
|
||||||
Cloud (from $9)
|
Cloud
|
||||||
</a>
|
</a>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -232,12 +232,12 @@ export const MainScreenComponent = () => {
|
|||||||
|
|
||||||
{!IS_CLOUD && (
|
{!IS_CLOUD && (
|
||||||
<a
|
<a
|
||||||
className="!text-black !underline !decoration-blue-600 !decoration-2 underline-offset-2 hover:opacity-80 dark:!text-gray-200"
|
className="!text-black hover:opacity-80 dark:!text-gray-200"
|
||||||
href="https://databasus.com/cloud"
|
href="https://databasus.com/cloud"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noreferrer"
|
rel="noreferrer"
|
||||||
>
|
>
|
||||||
Cloud (from $9)
|
Cloud
|
||||||
</a>
|
</a>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -211,7 +211,7 @@ export const SidebarComponent = ({
|
|||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noreferrer"
|
rel="noreferrer"
|
||||||
>
|
>
|
||||||
Cloud (from $9)
|
Cloud
|
||||||
</a>
|
</a>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user