Pull to refresh

Go + Minio: как написать простой сервер для взаимодействия с файлами

Level of difficultyMedium
Reading time26 min
Views5.6K

Добрейшего! Недавно мой друг решил хранить картинки продуктов на сервере, в отдельной папочке, выдав ей публичный доступ. Что ж, эту статью я пишу чтобы рассказать другу плюсы и минусы, а так же показать как можно делать иначе.

Если вы только изучаете go, начинаете писать сервера, то обязательно посмотрите эту статью - для бекендера уметь работать с s3 хранилищем так же важно, как и уметь работать с реляционной / нереляционной базой данных и с key-value базой - это основа основ.

одно из самых доступных s3 хранилищ конечно же minio
одно из самых доступных s3 хранилищ конечно же minio

Хранение файлов на сервере

Разберемся почему же, все-таки, файлики важно хранить именно в s3 хранилище, а не на сервере

  1. Безопасность данных:

    • Пример: Конфиденциальные документы (подпись об NDA) сотрудников находятся в публичной папке. Хакеры получают доступ и крадут личные данные, что приводит к утечке и юридическим последствиям.

  2. Уязвимость к атакам:

    • Пример: Публичные изображения товаров на сервере подвергаются DDoS-атаке, делая сайт недоступным и приводя к потере продаж.

  3. Отсутствие контроля версий и управления доступом:

    • Пример: Публичные изображения товаров хранятся в папке на сервере. В случае случайного удаления или изменения изображений нет возможности восстановить предыдущие версии. Это приводит к потере важных данных, чего можно избежать с использованием системы контроля версий в облачном хранилище, таком как S3.

  4. Нехватка масштабируемости:

    • Пример: Пользовательские данные хранятся на одном сервере. По мере роста данных сервер перегружается, замедляя работу приложения. Облачные решения, такие как S3, позволяют легко масштабировать хранилище.

  5. Управление резервными копиями и восстановлением:

    • Пример: Важные документы хранятся на локальном сервере. Аппаратный сбой приводит к потере данных, так как резервное копирование не проводилось. В облачном хранилище, таком как S3, резервные копии создаются автоматически.

Итого, использование специализированных решений для хранения данных, таких как S3, обеспечивает более высокий уровень безопасности, гибкости и управляемости, что делает их предпочтительным выбором для большинства приложений.

В нашем примере мы будем использовать minio и вот почему:

Эти проблемы, например, можно обозначить при предложении компании перейти на S3 хранилища, что покажет вашу компетентность в теме.

Далее о том как это делается.

Интеграция S3 хранилища на языке Golang

Итак, что мы сделаем в этом туториале:

  • Напишем простой web server с использованием gin

  • Добавим библиотеку для работы с переменными окружения, напишем config файл, который можно будет переиспользовать

  • Напишем minio client - клиент, отвечающий за подключение и работу с S3 хранилищем minio

  • Напишем 6 методов взаимодействия с S3 хранилищем minio

    • CreateOne

    • CreateMany

    • GetOne

    • GetMany

    • DeleteOne

    • DeleteMany

  • Напишем 6 хендлеров для взаимодействия с методами

Так мы получим небольшой проект, который можно будет реиспользовать в других проектах, научимся работать с S3 на Go, а конкретно с Minio

  1. Создание структуры проекта, подготовка окружения:

    Тыкс - структура проекта. Можно просто вставить список команд и у вас появится такая же.

    minio-gin-crud/
    ├── cmd/
    │ └── main.go
    ├── internal/
    │ ├── common/
    │ │ ├── config/
    │ │ │ └── config.go
    │ │ ├── dto/
    │ │ │ └── minio.go
    │ │ └── errors/
    │ │ └── errors.go
    │ │ └── responses/
    │ │ └── responses.go
    │ ├── handler/
    │ │ ├── minio/
    │ │ │ ├── handler.go
    │ │ │ └── minio.go
    │ │ └── handler.go
    │ ├── service/
    │ │ ├── minio/
    │ │ │ ├── minio.go
    │ │ │ └── service.go
    │ │ └── service.go
    ├── pkg/
    │ └── helpers/
    │ │ └── create-response.go
    │ │ └── file-data.type.go
    │ │ └── operation.error.go
    │ └── minio_client.go
    │ └── minio_service.go
    ├── .env
    ├── README.md
    ├── docker-compose.yml
    └── go.mod

    mkdir minio-gin-crud
    cd minio-gin-crud
    
    mkdir cmd
    mkdir internal
    mkdir pkg/
    mkdir pkg/minio
    mkdir pkg/minio/helpers
    
    mkdir internal/common
    mkdir internal/handler
    
    mkdir internal/common/dto 
    mkdir internal/common/errors
    mkdir internal/common/responses
    mkdir internal/common/config
    
    mkdir internal/handler/
    mkdir internal/handler/minioHandler
    
    touch cmd/main.go
    touch pkg/minio/minio_client.go
    touch pkg/minio/minio_service.go
    touch pkg/minio/helpers/operation.error.go
    touch pkg/minio/helpers/file-data.type.go
    
    touch internal/handler/handler.go
    touch internal/handler/minio/handler.go
    touch internal/handler/minio/minio.go
    
    touch internal/common/errors/errors.go
    touch internal/common/responses/responses.go
    touch internal/common/config/config.go
    touch internal/common/dto/minio.go
    
    touch README.md
    touch docker-compose.yml
    touch .env
    
    go mod init minio-gin-crud
    
    go get github.com/gin-gonic/gin
    go get github.com/minio/minio-go/v7
    go get github.com/joho/godotenv
  2. Docker-compose или как развернуть S3 хранилище

    Вот пример простого docker-compose файла с развертыванием Minio в качестве S3 хранилища: объяснять что-то на этом моменте - смысла не вижу, так как, кажется, здесь все предельно понятно

    version: '3'
    
    services:
      minio:
        container_name: minio
        image: 'bitnami/minio:latest'
        volumes:
          - 'minio_data:/data'
        ports:
          - "9000:9000"
        restart: unless-stopped
        environment:
          MINIO_ROOT_USER: "${MINIO_ROOT_USER}"
          MINIO_ROOT_PASSWORD: "${MINIO_ROOT_PASSWORD}"
          MINIO_USE_SSL: "${MINIO_USE_SSL}"
          MINIO_DEFAULT_BUCKETS: "${MINIO_BUCKET_NAME}"
        healthcheck:
          test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/live"]
          interval: 30s
          timeout: 20s
          retries: 3
    
    volumes:
      minio_data:

    Команды для запуска думаю вам знакома:
    docker-compose up --build
    Дальше круче: продолжаем

  3. Main server && Godotenv && Minio client

    Теперь то и начинается программирование.
    Первое с чего можно начать - это с подключения библиотеки godotenv - она, как уже говорилось ранее, нужна для простого взаимодействия с переменными окружения в Golang. Далее в комментариях будет описан каждый шаг:

    package config
    
    import (
    	"github.com/joho/godotenv"
    	"log"
    	"os"
    	"strconv"
    )
    
    // Config структура, обозначающая структуру .env файла
    type Config struct {
    	Port              string // Порт, на котором запускается сервер
    	MinioEndpoint     string // Адрес конечной точки Minio
    	BucketName        string // Название конкретного бакета в Minio
    	MinioRootUser     string // Имя пользователя для доступа к Minio
    	MinioRootPassword string // Пароль для доступа к Minio
    	MinioUseSSL       bool   // Переменная, отвечающая за
    }
    
    var AppConfig *Config
    
    // LoadConfig загружает конфигурацию из файла .env
    func LoadConfig() {
    	// Загружаем переменные окружения из файла .env
    	err := godotenv.Load()
    	if err != nil {
    		log.Fatalf("Error loading .env file")
    	}
    
    	// Устанавливаем конфигурационные параметры
    	AppConfig = &Config{
    		Port: getEnv("PORT", "8080"),
    
    		MinioEndpoint:     getEnv("MINIO_ENDPOINT", "localhost:9000"),
    		BucketName:        getEnv("MINIO_BUCKET_NAME", "defaultBucket"),
    		MinioRootUser:     getEnv("MINIO_ROOT_USER", "root"),
    		MinioRootPassword: getEnv("MINIO_ROOT_PASSWORD", "minio_password"),
    		MinioUseSSL:       getEnvAsBool("MINIO_USE_SSL", false),
    	}
    }
    
    // getEnv считывает значение переменной окружения или возвращает значение по умолчанию, если переменная не установлена
    func getEnv(key string, defaultValue string) string {
    	if value, exists := os.LookupEnv(key); exists {
    		return value
    	}
    	return defaultValue
    }
    
    // getEnvAsInt считывает значение переменной окружения как целое число или возвращает значение по умолчанию, если переменная не установлена или не может быть преобразована в целое число
    func getEnvAsInt(key string, defaultValue int) int {
    	if valueStr := getEnv(key, ""); valueStr != "" {
    		if value, err := strconv.Atoi(valueStr); err == nil {
    			return value
    		}
    	}
    	return defaultValue
    }
    
    // getEnvAsBool считывает значение переменной окружения как булево или возвращает значение по умолчанию, если переменная не установлена или не может быть преобразована в булево
    func getEnvAsBool(key string, defaultValue bool) bool {
    	if valueStr := getEnv(key, ""); valueStr != "" {
    		if value, err := strconv.ParseBool(valueStr); err == nil {
    			return value
    		}
    	}
    	return defaultValue
    }
    

    Теперь, когда у нас есть конфиг, мы можем создать main.go и minio_client.go.

    Давайте создадим minio_client.go в первую очередь, а так же интерфейс этого клиента со всеми сопутствующими методами:

    package minio
    
    import (
    	"context"
    	"github.com/minio/minio-go/v7"
    	"github.com/minio/minio-go/v7/pkg/credentials"
    	"minio-gin-crud/internal/common/config"
    	"minio-gin-crud/pkg/minio/helpers"
    )
    
    // Client интерфейс для взаимодействия с Minio
    type Client interface {
    	InitMinio() error                                             // Метод для инициализации подключения к Minio
    	CreateOne(file helpers.FileDataType) (string, error)          // Метод для создания одного объекта в бакете Minio
    	CreateMany(map[string]helpers.FileDataType) ([]string, error) // Метод для создания нескольких объектов в бакете Minio
    	GetOne(objectID string) (string, error)                       // Метод для получения одного объекта из бакета Minio
    	GetMany(objectIDs []string) ([]string, error)                 // Метод для получения нескольких объектов из бакета Minio
    	DeleteOne(objectID string) error                              // Метод для удаления одного объекта из бакета Minio
    	DeleteMany(objectIDs []string) error                          // Метод для удаления нескольких объектов из бакета Minio
    }
    
    // minioClient реализация интерфейса MinioClient
    type minioClient struct {
    	mc *minio.Client // Клиент Minio
    }
    
    // NewMinioClient создает новый экземпляр Minio Client
    func NewMinioClient() Client {
    	return &minioClient{} // Возвращает новый экземпляр minioClient с указанным именем бакета
    }
    
    // InitMinio подключается к Minio и создает бакет, если не существует
    // Бакет - это контейнер для хранения объектов в Minio. Он представляет собой пространство имен, в котором можно хранить и организовывать файлы и папки.
    func (m *minioClient) InitMinio() error {
    	// Создание контекста с возможностью отмены операции
    	ctx := context.Background()
    
    	// Подключение к Minio с использованием имени пользователя и пароля
    	client, err := minio.New(config.AppConfig.MinioEndpoint, &minio.Options{
    		Creds:  credentials.NewStaticV4(config.AppConfig.MinioRootUser, config.AppConfig.MinioRootPassword, ""),
    		Secure: config.AppConfig.MinioUseSSL,
    	})
    	if err != nil {
    		return err
    	}
    
    	// Установка подключения Minio
    	m.mc = client
    
    	// Проверка наличия бакета и его создание, если не существует
    	exists, err := m.mc.BucketExists(ctx, config.AppConfig.BucketName)
    	if err != nil {
    		return err
    	}
    	if !exists {
    		err := m.mc.MakeBucket(ctx, config.AppConfig.BucketName, minio.MakeBucketOptions{})
    		if err != nil {
    			return err
    		}
    	}
    
    	return nil
    }
    

    Теперь осталось написать main.go - файл, который будет запускать весь проект:

    package main
    
    import (
    	"github.com/gin-gonic/gin"
    	"github.com/joho/godotenv"
    	"log"
    	"minio-gin-crud/internal/common/config"
    	"minio-gin-crud/pkg/minio"
    )
    
    func main() {
    	// Загрузка конфигурации из файла .env
    	err := godotenv.Load()
    	if err != nil {
    		log.Fatalf("Ошибка загрузки файла .env: %v", err)
    	}
    
    	// Инициализация соединения с Minio
    	minioClient := minio.NewMinioClient()
    	err = minioClient.InitMinio()
    	if err != nil {
    		log.Fatalf("Ошибка инициализации Minio: %v", err)
    	}
    
    	// Инициализация маршрутизатора Gin
    	router := gin.Default()
    
    	// Запуск сервера Gin
    	port := config.AppConfig.Port // Мы берем
    	err = router.Run(":" + port)
    	if err != nil {
    		log.Fatalf("Ошибка запуска сервера Gin: %v", err)
    	}
    }
    

    У нас получился вполне простой main.go файл. Далее сюда нам надо будет добавить еще обработку хендлеров, но это позже. Теперь нам нужно описать интерфейс minio.Client - добавить в minio_service.go все недостающие методы, потому что до текущего момента проект нельзя будет запустить.

  4. Minio service

    Итак, тут му будем описывать все методы взаимодействия с Minio. Каждый метод будет прокомментирован, но, тем не менее, если останется что-то непонятное - пишите в комментарии я или кто-то другой обязательно вам помогут!

    1. CreateOne

      // CreateOne создает один объект в бакете Minio.
      // Метод принимает структуру fileData, которая содержит имя файла и его данные.
      // В случае успешной загрузки данных в бакет, метод возвращает nil, иначе возвращает ошибку.
      // Все операции выполняются в контексте задачи.
      func (m *minioClient) CreateOne(file helpers.FileDataType) (string, error) {
      	// Генерация уникального идентификатора для нового объекта.
      	objectID := uuid.New().String()
      
      	// Создание потока данных для загрузки в бакет Minio.
      	reader := bytes.NewReader(file.Data)
      
      	// Загрузка данных в бакет Minio с использованием контекста для возможности отмены операции.
      	_, err := m.mc.PutObject(context.Background(), config.AppConfig.BucketName, objectID, reader, int64(len(file.Data)), minio.PutObjectOptions{})
      	if err != nil {
      		return "", fmt.Errorf("ошибка при создании объекта %s: %v", file.FileName, err)
      	}
      
      	// Получение URL для загруженного объекта
      	url, err := m.mc.PresignedGetObject(context.Background(), config.AppConfig.BucketName, objectID, time.Second*24*60*60, nil)
      	if err != nil {
      		return "", fmt.Errorf("ошибка при создании URL для объекта %s: %v", file.FileName, err)
      	}
      
      	return url.String(), nil
      }
    2. CreateMany

      // CreateMany создает несколько объектов в хранилище MinIO из переданных данных.
      // Если происходит ошибка при создании объекта, метод возвращает ошибку,
      // указывающую на неудачные объекты.
      func (m *minioClient) CreateMany(data map[string]helpers.FileDataType) ([]string, error) {
      	urls := make([]string, 0, len(data)) // Массив для хранения URL-адресов
      
      	ctx, cancel := context.WithCancel(context.Background()) // Создание контекста с возможностью отмены операции.
      	defer cancel()                                          // Отложенный вызов функции отмены контекста при завершении функции CreateMany.
      
      	// Создание канала для передачи URL-адресов с размером, равным количеству переданных данных.
      	urlCh := make(chan string, len(data))
      
      	var wg sync.WaitGroup // WaitGroup для ожидания завершения всех горутин.
      
      	// Запуск горутин для создания каждого объекта.
      	for objectID, file := range data {
      		wg.Add(1) // Увеличение счетчика WaitGroup перед запуском каждой горутины.
      		go func(objectID string, file helpers.FileDataType) {
      			defer wg.Done()                                                                                                                                   // Уменьшение счетчика WaitGroup после завершения горутины.
      			_, err := m.mc.PutObject(ctx, config.AppConfig.BucketName, objectID, bytes.NewReader(file.Data), int64(len(file.Data)), minio.PutObjectOptions{}) // Создание объекта в бакете MinIO.
      			if err != nil {
      				cancel() // Отмена операции при возникновении ошибки.
      				return
      			}
      
      			// Получение URL для загруженного объекта
      			url, err := m.mc.PresignedGetObject(ctx, config.AppConfig.BucketName, objectID, time.Second*24*60*60, nil)
      			if err != nil {
      				cancel() // Отмена операции при возникновении ошибки.
      				return
      			}
      
      			urlCh <- url.String() // Отправка URL-адреса в канал с URL-адресами.
      		}(objectID, file) // Передача данных объекта в анонимную горутину.
      	}
      
      	// Ожидание завершения всех горутин и закрытие канала с URL-адресами.
      	go func() {
      		wg.Wait()    // Блокировка до тех пор, пока счетчик WaitGroup не станет равным 0.
      		close(urlCh) // Закрытие канала с URL-адресами после завершения всех горутин.
      	}()
      
      	// Сбор URL-адресов из канала.
      	for url := range urlCh {
      		urls = append(urls, url) // Добавление URL-адреса в массив URL-адресов.
      	}
      
      	return urls, nil
      }
    3. GetOne

      // GetOne получает один объект из бакета Minio по его идентификатору.
      // Он принимает строку `objectID` в качестве параметра и возвращает срез байт данных объекта и ошибку, если такая возникает.
      func (m *minioClient) GetOne(objectID string) (string, error) {
      	// Получение предварительно подписанного URL для доступа к объекту Minio.
      	url, err := m.mc.PresignedGetObject(context.Background(), config.AppConfig.BucketName, objectID, time.Second*24*60*60, nil)
      	if err != nil {
      		return "", fmt.Errorf("ошибка при получении URL для объекта %s: %v", objectID, err)
      	}
      
      	return url.String(), nil
      }
    4. GetMany

      // GetMany получает несколько объектов из бакета Minio по их идентификаторам.
      func (m *minioClient) GetMany(objectIDs []string) ([]string, error) {
      	// Создание каналов для передачи URL-адресов объектов и ошибок
      	urlCh := make(chan string, len(objectIDs))                 // Канал для URL-адресов объектов
      	errCh := make(chan helpers.OperationError, len(objectIDs)) // Канал для ошибок
      
      	var wg sync.WaitGroup                                 // WaitGroup для ожидания завершения всех горутин
      	_, cancel := context.WithCancel(context.Background()) // Создание контекста с возможностью отмены операции
      	defer cancel()                                        // Отложенный вызов функции отмены контекста при завершении функции GetMany
      
      	// Запуск горутин для получения URL-адресов каждого объекта.
      	for _, objectID := range objectIDs {
      		wg.Add(1) // Увеличение счетчика WaitGroup перед запуском каждой горутины
      		go func(objectID string) {
      			defer wg.Done()                // Уменьшение счетчика WaitGroup после завершения горутины
      			url, err := m.GetOne(objectID) // Получение URL-адреса объекта по его идентификатору с помощью метода GetOne
      			if err != nil {
      				errCh <- helpers.OperationError{ObjectID: objectID, Error: fmt.Errorf("ошибка при получении объекта %s: %v", objectID, err)} // Отправка ошибки в канал с ошибками
      				cancel()                                                                                                                     // Отмена операции при возникновении ошибки
      				return
      			}
      			urlCh <- url // Отправка URL-адреса объекта в канал с URL-адресами
      		}(objectID) // Передача идентификатора объекта в анонимную горутину
      	}
      
      	// Закрытие каналов после завершения всех горутин.
      	go func() {
      		wg.Wait()    // Блокировка до тех пор, пока счетчик WaitGroup не станет равным 0
      		close(urlCh) // Закрытие канала с URL-адресами после завершения всех горутин
      		close(errCh) // Закрытие канала с ошибками после завершения всех горутин
      	}()
      
      	// Сбор URL-адресов объектов и ошибок из каналов.
      	var urls []string // Массив для хранения URL-адресов
      	var errs []error  // Массив для хранения ошибок
      	for url := range urlCh {
      		urls = append(urls, url) // Добавление URL-адреса в массив URL-адресов
      	}
      	for opErr := range errCh {
      		errs = append(errs, opErr.Error) // Добавление ошибки в массив ошибок
      	}
      
      	// Проверка наличия ошибок.
      	if len(errs) > 0 {
      		return nil, fmt.Errorf("ошибки при получении объектов: %v", errs) // Возврат ошибки, если возникли ошибки при получении объектов
      	}
      
      	return urls, nil // Возврат массива URL-адресов, если ошибок не возникло
      }
    5. DeleteOne

      // DeleteOne удаляет один объект из бакета Minio по его идентификатору.
      func (m *minioClient) DeleteOne(objectID string) error {
      	// Удаление объекта из бакета Minio.
      	err := m.mc.RemoveObject(context.Background(), config.AppConfig.BucketName, objectID, minio.RemoveObjectOptions{})
      	if err != nil {
      		return err // Возвращаем ошибку, если не удалось удалить объект.
      	}
      	return nil // Возвращаем nil, если объект успешно удалён.
      }
    6. DeleteMany

      // DeleteMany удаляет несколько объектов из бакета Minio по их идентификаторам с использованием горутин.
      func (m *minioClient) DeleteMany(objectIDs []string) error {
      	// Создание канала для передачи ошибок с размером, равным количеству объектов для удаления
      	errCh := make(chan helpers.OperationError, len(objectIDs)) // Канал для ошибок
      	var wg sync.WaitGroup                                      // WaitGroup для ожидания завершения всех горутин
      
      	ctx, cancel := context.WithCancel(context.Background()) // Создание контекста с возможностью отмены операции
      	defer cancel()                                          // Отложенный вызов функции отмены контекста при завершении функции DeleteMany
      
      	// Запуск горутин для удаления каждого объекта.
      	for _, objectID := range objectIDs {
      		wg.Add(1) // Увеличение счетчика WaitGroup перед запуском каждой горутины
      		go func(id string) {
      			defer wg.Done()                                                                             // Уменьшение счетчика WaitGroup после завершения горутины
      			err := m.mc.RemoveObject(ctx, config.AppConfig.BucketName, id, minio.RemoveObjectOptions{}) // Удаление объекта с использованием Minio клиента
      			if err != nil {
      				errCh <- helpers.OperationError{ObjectID: id, Error: fmt.Errorf("ошибка при удалении объекта %s: %v", id, err)} // Отправка ошибки в канал с ошибками
      				cancel()                                                                                                        // Отмена операции при возникновении ошибки
      			}
      		}(objectID) // Передача идентификатора объекта в анонимную горутину
      	}
      
      	// Ожидание завершения всех горутин и закрытие канала с ошибками.
      	go func() {
      		wg.Wait()    // Блокировка до тех пор, пока счетчик WaitGroup не станет равным 0
      		close(errCh) // Закрытие канала с ошибками после завершения всех горутин
      	}()
      
      	// Сбор ошибок из канала.
      	var errs []error // Массив для хранения ошибок
      	for opErr := range errCh {
      		errs = append(errs, opErr.Error) // Добавление ошибки в массив ошибок
      	}
      
      	// Проверка наличия ошибок.
      	if len(errs) > 0 {
      		return fmt.Errorf("ошибки при удалении объектов: %v", errs) // Возврат ошибки, если возникли ошибки при удалении объектов
      	}
      
      	return nil // Возврат nil, если ошибок не возникло
      }
      
    7. Полный файл:

    package minio
    
    import (
    	"bytes"
    	"context"
    	"fmt"
    	"github.com/google/uuid"
    	"github.com/minio/minio-go/v7"
    	"minio-gin-crud/internal/common/config"
    	"minio-gin-crud/pkg/minio/helpers"
    	"sync"
    	"time"
    )
    
    // Контекст используется для передачи сигналов об отмене операции загрузки в случае необходимости.
    
    // CreateOne создает один объект в бакете Minio.
    // Метод принимает структуру fileData, которая содержит имя файла и его данные.
    // В случае успешной загрузки данных в бакет, метод возвращает nil, иначе возвращает ошибку.
    // Все операции выполняются в контексте задачи.
    func (m *minioClient) CreateOne(file helpers.FileDataType) (string, error) {
    	// Генерация уникального идентификатора для нового объекта.
    	objectID := uuid.New().String()
    
    	// Создание потока данных для загрузки в бакет Minio.
    	reader := bytes.NewReader(file.Data)
    
    	// Загрузка данных в бакет Minio с использованием контекста для возможности отмены операции.
    	_, err := m.mc.PutObject(context.Background(), config.AppConfig.BucketName, objectID, reader, int64(len(file.Data)), minio.PutObjectOptions{})
    	if err != nil {
    		return "", fmt.Errorf("ошибка при создании объекта %s: %v", file.FileName, err)
    	}
    
    	// Получение URL для загруженного объекта
    	url, err := m.mc.PresignedGetObject(context.Background(), config.AppConfig.BucketName, objectID, time.Second*24*60*60, nil)
    	if err != nil {
    		return "", fmt.Errorf("ошибка при создании URL для объекта %s: %v", file.FileName, err)
    	}
    
    	return url.String(), nil
    }
    
    // CreateMany создает несколько объектов в хранилище MinIO из переданных данных.
    // Если происходит ошибка при создании объекта, метод возвращает ошибку,
    // указывающую на неудачные объекты.
    func (m *minioClient) CreateMany(data map[string]helpers.FileDataType) ([]string, error) {
    	urls := make([]string, 0, len(data)) // Массив для хранения URL-адресов
    
    	ctx, cancel := context.WithCancel(context.Background()) // Создание контекста с возможностью отмены операции.
    	defer cancel()                                          // Отложенный вызов функции отмены контекста при завершении функции CreateMany.
    
    	// Создание канала для передачи URL-адресов с размером, равным количеству переданных данных.
    	urlCh := make(chan string, len(data))
    
    	var wg sync.WaitGroup // WaitGroup для ожидания завершения всех горутин.
    
    	// Запуск горутин для создания каждого объекта.
    	for objectID, file := range data {
    		wg.Add(1) // Увеличение счетчика WaitGroup перед запуском каждой горутины.
    		go func(objectID string, file helpers.FileDataType) {
    			defer wg.Done()                                                                                                                                   // Уменьшение счетчика WaitGroup после завершения горутины.
    			_, err := m.mc.PutObject(ctx, config.AppConfig.BucketName, objectID, bytes.NewReader(file.Data), int64(len(file.Data)), minio.PutObjectOptions{}) // Создание объекта в бакете MinIO.
    			if err != nil {
    				cancel() // Отмена операции при возникновении ошибки.
    				return
    			}
    
    			// Получение URL для загруженного объекта
    			url, err := m.mc.PresignedGetObject(ctx, config.AppConfig.BucketName, objectID, time.Second*24*60*60, nil)
    			if err != nil {
    				cancel() // Отмена операции при возникновении ошибки.
    				return
    			}
    
    			urlCh <- url.String() // Отправка URL-адреса в канал с URL-адресами.
    		}(objectID, file) // Передача данных объекта в анонимную горутину.
    	}
    
    	// Ожидание завершения всех горутин и закрытие канала с URL-адресами.
    	go func() {
    		wg.Wait()    // Блокировка до тех пор, пока счетчик WaitGroup не станет равным 0.
    		close(urlCh) // Закрытие канала с URL-адресами после завершения всех горутин.
    	}()
    
    	// Сбор URL-адресов из канала.
    	for url := range urlCh {
    		urls = append(urls, url) // Добавление URL-адреса в массив URL-адресов.
    	}
    
    	return urls, nil
    }
    
    // GetOne получает один объект из бакета Minio по его идентификатору.
    // Он принимает строку `objectID` в качестве параметра и возвращает срез байт данных объекта и ошибку, если такая возникает.
    func (m *minioClient) GetOne(objectID string) (string, error) {
    	// Получение предварительно подписанного URL для доступа к объекту Minio.
    	url, err := m.mc.PresignedGetObject(context.Background(), config.AppConfig.BucketName, objectID, time.Second*24*60*60, nil)
    	if err != nil {
    		return "", fmt.Errorf("ошибка при получении URL для объекта %s: %v", objectID, err)
    	}
    
    	return url.String(), nil
    }
    
    // GetMany получает несколько объектов из бакета Minio по их идентификаторам.
    func (m *minioClient) GetMany(objectIDs []string) ([]string, error) {
    	// Создание каналов для передачи URL-адресов объектов и ошибок
    	urlCh := make(chan string, len(objectIDs))                 // Канал для URL-адресов объектов
    	errCh := make(chan helpers.OperationError, len(objectIDs)) // Канал для ошибок
    
    	var wg sync.WaitGroup                                 // WaitGroup для ожидания завершения всех горутин
    	_, cancel := context.WithCancel(context.Background()) // Создание контекста с возможностью отмены операции
    	defer cancel()                                        // Отложенный вызов функции отмены контекста при завершении функции GetMany
    
    	// Запуск горутин для получения URL-адресов каждого объекта.
    	for _, objectID := range objectIDs {
    		wg.Add(1) // Увеличение счетчика WaitGroup перед запуском каждой горутины
    		go func(objectID string) {
    			defer wg.Done()                // Уменьшение счетчика WaitGroup после завершения горутины
    			url, err := m.GetOne(objectID) // Получение URL-адреса объекта по его идентификатору с помощью метода GetOne
    			if err != nil {
    				errCh <- helpers.OperationError{ObjectID: objectID, Error: fmt.Errorf("ошибка при получении объекта %s: %v", objectID, err)} // Отправка ошибки в канал с ошибками
    				cancel()                                                                                                                     // Отмена операции при возникновении ошибки
    				return
    			}
    			urlCh <- url // Отправка URL-адреса объекта в канал с URL-адресами
    		}(objectID) // Передача идентификатора объекта в анонимную горутину
    	}
    
    	// Закрытие каналов после завершения всех горутин.
    	go func() {
    		wg.Wait()    // Блокировка до тех пор, пока счетчик WaitGroup не станет равным 0
    		close(urlCh) // Закрытие канала с URL-адресами после завершения всех горутин
    		close(errCh) // Закрытие канала с ошибками после завершения всех горутин
    	}()
    
    	// Сбор URL-адресов объектов и ошибок из каналов.
    	var urls []string // Массив для хранения URL-адресов
    	var errs []error  // Массив для хранения ошибок
    	for url := range urlCh {
    		urls = append(urls, url) // Добавление URL-адреса в массив URL-адресов
    	}
    	for opErr := range errCh {
    		errs = append(errs, opErr.Error) // Добавление ошибки в массив ошибок
    	}
    
    	// Проверка наличия ошибок.
    	if len(errs) > 0 {
    		return nil, fmt.Errorf("ошибки при получении объектов: %v", errs) // Возврат ошибки, если возникли ошибки при получении объектов
    	}
    
    	return urls, nil // Возврат массива URL-адресов, если ошибок не возникло
    }
    
    // DeleteOne удаляет один объект из бакета Minio по его идентификатору.
    func (m *minioClient) DeleteOne(objectID string) error {
    	// Удаление объекта из бакета Minio.
    	err := m.mc.RemoveObject(context.Background(), config.AppConfig.BucketName, objectID, minio.RemoveObjectOptions{})
    	if err != nil {
    		return err // Возвращаем ошибку, если не удалось удалить объект.
    	}
    	return nil // Возвращаем nil, если объект успешно удалён.
    }
    
    // DeleteMany удаляет несколько объектов из бакета Minio по их идентификаторам с использованием горутин.
    func (m *minioClient) DeleteMany(objectIDs []string) error {
    	// Создание канала для передачи ошибок с размером, равным количеству объектов для удаления
    	errCh := make(chan helpers.OperationError, len(objectIDs)) // Канал для ошибок
    	var wg sync.WaitGroup                                      // WaitGroup для ожидания завершения всех горутин
    
    	ctx, cancel := context.WithCancel(context.Background()) // Создание контекста с возможностью отмены операции
    	defer cancel()                                          // Отложенный вызов функции отмены контекста при завершении функции DeleteMany
    
    	// Запуск горутин для удаления каждого объекта.
    	for _, objectID := range objectIDs {
    		wg.Add(1) // Увеличение счетчика WaitGroup перед запуском каждой горутины
    		go func(id string) {
    			defer wg.Done()                                                                             // Уменьшение счетчика WaitGroup после завершения горутины
    			err := m.mc.RemoveObject(ctx, config.AppConfig.BucketName, id, minio.RemoveObjectOptions{}) // Удаление объекта с использованием Minio клиента
    			if err != nil {
    				errCh <- helpers.OperationError{ObjectID: id, Error: fmt.Errorf("ошибка при удалении объекта %s: %v", id, err)} // Отправка ошибки в канал с ошибками
    				cancel()                                                                                                        // Отмена операции при возникновении ошибки
    			}
    		}(objectID) // Передача идентификатора объекта в анонимную горутину
    	}
    
    	// Ожидание завершения всех горутин и закрытие канала с ошибками.
    	go func() {
    		wg.Wait()    // Блокировка до тех пор, пока счетчик WaitGroup не станет равным 0
    		close(errCh) // Закрытие канала с ошибками после завершения всех горутин
    	}()
    
    	// Сбор ошибок из канала.
    	var errs []error // Массив для хранения ошибок
    	for opErr := range errCh {
    		errs = append(errs, opErr.Error) // Добавление ошибки в массив ошибок
    	}
    
    	// Проверка наличия ошибок.
    	if len(errs) > 0 {
    		return fmt.Errorf("ошибки при удалении объектов: %v", errs) // Возврат ошибки, если возникли ошибки при удалении объектов
    	}
    
    	return nil // Возврат nil, если ошибок не возникло
    }
    
    1. helpers : error | type

      package helpers
      
      type FileDataType struct {
      	FileName string
      	Data     []byte
      }
      
      ////////// в разных файлах в папочке helpers //////////
      
      package helpers
      
      type OperationError struct {
      	ObjectID string
      	Error    error
      }
      

      На этом сервис Minio готов - осталось только написать хендлеры и можно запускать проект!

      На текущем этапе его можно и запустить:
      -- запустить docker-compose : docker-compose up --build
      -- запустить сервер : go run cmd/main.go

      Но тестировать нечего - надо писать хендлеры - давай займемся этим

  5. Minio handlers

    Для начала необходимо описать основные структуры, которые му будем использовать:
    - Структура хендлера в пакете minioHandler - показывает что должен принять в себя хендлер, какие сервисы он будет использовать (только minio service - что неудивительно)

    package minioHandler
    
    import "minio-gin-crud/pkg/minio"
    
    type Handler struct {
    	minioService minio.Client
    }
    
    func NewMinioHandler(
    	minioService minio.Client,
    ) *Handler {
    	return &Handler{
    		minioService: minioService,
    	}
    }
    

    Основной handler, отвечающий также за регистрацию роутов:

    package handler
    
    import (
    	"github.com/gin-gonic/gin"
    	"minio-gin-crud/internal/handler/minioHandler"
    	"minio-gin-crud/pkg/minio"
    )
    
    // Services структура всех сервисов, которые используются в хендлерах
    // Это нужно чтобы мы могли использовать внутри хендлеров эти самые сервисы
    type Services struct {
    	minioService minio.Client // Сервис у нас только один - minio, мы планируем его использовать, поэтому передаем
    }
    
    // Handlers структура всех хендлеров, которые используются для обозначения действия в роутах
    type Handlers struct {
    	minioHandler minioHandler.Handler // Пока у нас только один роут
    }
    
    // NewHandler создает экземпляр Handler с предоставленными сервисами
    func NewHandler(
    	minioService minio.Client,
    ) (*Services, *Handlers) {
    	return &Services{
    			minioService: minioService,
    		}, &Handlers{
    			// инициируем Minio handler, который на вход получает minio service
    			minioHandler: *minioHandler.NewMinioHandler(minioService),
    		}
    }
    
    // RegisterRoutes - метод регистрации всех роутов в системе
    func (h *Handlers) RegisterRoutes(router *gin.Engine) {
    
    	// Здесь мы обозначили все эндпоинты системы с соответствующими хендлерами
    	minioRoutes := router.Group("/files")
    	{
    		minioRoutes.POST("/", h.minioHandler.CreateOne)
    		minioRoutes.POST("/many", h.minioHandler.CreateMany)
    
    		minioRoutes.GET("/:objectID", h.minioHandler.GetOne)
    		minioRoutes.GET("/many", h.minioHandler.GetMany)
    		
    		minioRoutes.DELETE("/:objectID", h.minioHandler.DeleteOne)
    		minioRoutes.DELETE("/many", h.minioHandler.DeleteMany)
    	}
    
    }

    Основные типы данных в файлах errors / responses / dto

    package dto
    
    // Нужен когда в body приходит много objectId - GetMany / DeleteMany
    type ObjectIdsDto struct {
    	ObjectIDs []string `json:"objectIDs"`
    }
    
    ////
    
    package errors
    
    // Нужен для JSON ответов в случае неправильной работы сервиса
    type ErrorResponse struct {
    	Error   string      `json:"error"`
    	Status  int         `json:"code,omitempty"`
    	Details interface{} `json:"details,omitempty"`
    }
    
    ////
    
    package responses
    
    // Нужен для JSON ответов в случае правильной работы сервиса
    type SuccessResponse struct {
    	Status  int         `json:"status"`
    	Message string      `json:"message"`
    	Data    interface{} `json:"data,omitempty"`
    }
    

    Теперь можно писать handlers - я так же буду прилагать скрины из постмана как я протестировал эти методы:

    1. CreateOne

      // CreateOne обработчик для создания одного объекта в хранилище MinIO из переданных данных.
      func (h *Handler) CreateOne(c *gin.Context) {
      	// Получаем файл из запроса
      	file, err := c.FormFile("file")
      	if err != nil {
      		// Если файл не получен, возвращаем ошибку с соответствующим статусом и сообщением
      		c.JSON(http.StatusBadRequest, errors.ErrorResponse{
      			Status:  http.StatusBadRequest,
      			Error:   "No file is received",
      			Details: err,
      		})
      		return
      	}
      
      	// Открываем файл для чтения
      	f, err := file.Open()
      	if err != nil {
      		// Если файл не удается открыть, возвращаем ошибку с соответствующим статусом и сообщением
      		c.JSON(http.StatusInternalServerError, errors.ErrorResponse{
      			Status:  http.StatusInternalServerError,
      			Error:   "Unable to open the file",
      			Details: err,
      		})
      		return
      	}
      	defer f.Close() // Закрываем файл после завершения работы с ним
      
      	// Читаем содержимое файла в байтовый срез
      	fileBytes, err := io.ReadAll(f)
      	if err != nil {
      		// Если не удается прочитать содержимое файла, возвращаем ошибку с соответствующим статусом и сообщением
      		c.JSON(http.StatusInternalServerError, errors.ErrorResponse{
      			Status:  http.StatusInternalServerError,
      			Error:   "Unable to read the file",
      			Details: err,
      		})
      		return
      	}
      
      	// Создаем структуру FileDataType для хранения данных файла
      	fileData := helpers.FileDataType{
      		FileName: file.Filename, // Имя файла
      		Data:     fileBytes,     // Содержимое файла в виде байтового среза
      	}
      
      	// Сохраняем файл в MinIO с помощью метода CreateOne
      	link, err := h.minioService.CreateOne(fileData)
      	if err != nil {
      		// Если не удается сохранить файл, возвращаем ошибку с соответствующим статусом и сообщением
      		c.JSON(http.StatusInternalServerError, errors.ErrorResponse{
      			Status:  http.StatusInternalServerError,
      			Error:   "Unable to save the file",
      			Details: err,
      		})
      		return
      	}
      
      	// Возвращаем успешный ответ с URL-адресом сохраненного файла
      	c.JSON(http.StatusOK, responses.SuccessResponse{
      		Status:  http.StatusOK,
      		Message: "File uploaded successfully",
      		Data:    link, // URL-адрес загруженного файла
      	})
      }

      Успешный результат:

      результаты мне нравятся
      результаты мне нравятся
    2. CreateMany

      // CreateMany обработчик для создания нескольких объектов в хранилище MinIO из переданных данных.
      func (h *Handler) CreateMany(c *gin.Context) {
      	// Получаем multipart форму из запроса
      	form, err := c.MultipartForm()
      	if err != nil {
      		// Если форма недействительна, возвращаем ошибку с соответствующим статусом и сообщением
      		c.JSON(http.StatusBadRequest, errors.ErrorResponse{
      			Status:  http.StatusBadRequest,
      			Error:   "Invalid form",
      			Details: err,
      		})
      		return
      	}
      
      	// Получаем файлы из формы
      	files := form.File["files"]
      	if files == nil {
      		// Если файлы не получены, возвращаем ошибку с соответствующим статусом и сообщением
      		c.JSON(http.StatusBadRequest, errors.ErrorResponse{
      			Status:  http.StatusBadRequest,
      			Error:   "No files are received",
      			Details: err,
      		})
      		return
      	}
      
      	// Создаем map для хранения данных файлов
      	data := make(map[string]helpers.FileDataType)
      
      	// Проходим по каждому файлу в форме
      	for _, file := range files {
      		// Открываем файл
      		f, err := file.Open()
      		if err != nil {
      			// Если файл не удается открыть, возвращаем ошибку с соответствующим статусом и сообщением
      			c.JSON(http.StatusInternalServerError, errors.ErrorResponse{
      				Status:  http.StatusInternalServerError,
      				Error:   "Unable to open the file",
      				Details: err,
      			})
      			return
      		}
      		defer f.Close() // Закрываем файл после завершения работы с ним
      
      		// Читаем содержимое файла в байтовый срез
      		fileBytes, err := io.ReadAll(f)
      		if err != nil {
      			// Если не удается прочитать содержимое файла, возвращаем ошибку с соответствующим статусом и сообщением
      			c.JSON(http.StatusInternalServerError, errors.ErrorResponse{
      				Status:  http.StatusInternalServerError,
      				Error:   "Unable to read the file",
      				Details: err,
      			})
      			return
      		}
      
      		// Добавляем данные файла в map
      		data[file.Filename] = helpers.FileDataType{
      			FileName: file.Filename, // Имя файла
      			Data:     fileBytes,     // Содержимое файла в виде байтового среза
      		}
      	}
      
      	// Сохраняем файлы в MinIO с помощью метода CreateMany
      	links, err := h.minioService.CreateMany(data)
      	if err != nil {
      		// Если не удается сохранить файлы, возвращаем ошибку с соответствующим статусом и сообщением
      		fmt.Printf("err: %+v\n ", err.Error())
      		c.JSON(http.StatusInternalServerError, errors.ErrorResponse{
      			Status:  http.StatusInternalServerError,
      			Error:   "Unable to save the files",
      			Details: err,
      		})
      		return
      	}
      
      	// Возвращаем успешный ответ с URL-адресами сохраненных файлов
      	c.JSON(http.StatusOK, responses.SuccessResponse{
      		Status:  http.StatusOK,
      		Message: "Files uploaded successfully",
      		Data:    links, // URL-адреса загруженных файлов
      	})
      }
      
      success
      success
    3. GetOne

      // GetOne обработчик для получения одного объекта из бакета Minio по его идентификатору.
      func (h *Handler) GetOne(c *gin.Context) {
      	// Получаем идентификатор объекта из параметров URL
      	objectID := c.Param("objectID")
      
      	// Используем сервис MinIO для получения ссылки на объект
      	link, err := h.minioService.GetOne(objectID)
      	if err != nil {
      		// Если произошла ошибка при получении объекта, возвращаем ошибку с соответствующим статусом и сообщением
      		c.JSON(http.StatusInternalServerError, errors.ErrorResponse{
      			Status:  http.StatusInternalServerError,
      			Error:   "Enable to get the object",
      			Details: err,
      		})
      		return
      	}
      
      	// Возвращаем успешный ответ с URL-адресом полученного файла
      	c.JSON(http.StatusOK, responses.SuccessResponse{
      		Status:  http.StatusOK,
      		Message: "File received successfully",
      		Data:    link, // URL-адрес полученного файла
      	})
      }
      
      успешный успех!
      успешный успех!
    4. GetMany

      // GetMany обработчик для получения нескольких объектов из бакета Minio по их идентификаторам.
      func (h *Handler) GetMany(c *gin.Context) {
      	// Объявление переменной для хранения получаемых идентификаторов объектов
      	var objectIDs dto.ObjectIdsDto
      
      	// Привязка JSON данных из запроса к переменной objectIDs
      	if err := c.ShouldBindJSON(&objectIDs); err != nil {
      		// Если привязка данных не удалась, возвращаем ошибку с соответствующим статусом и сообщением
      		c.JSON(http.StatusBadRequest, errors.ErrorResponse{
      			Status:  http.StatusBadRequest,
      			Error:   "Invalid request body",
      			Details: err,
      		})
      		return
      	}
      
      	// Используем сервис MinIO для получения ссылок на объекты по их идентификаторам
      	links, err := h.minioService.GetMany(objectIDs.ObjectIDs)
      	if err != nil {
      		// Если произошла ошибка при получении объектов, возвращаем ошибку с соответствующим статусом и сообщением
      		c.JSON(http.StatusInternalServerError, errors.ErrorResponse{
      			Status:  http.StatusInternalServerError,
      			Error:   "Enable to get many objects",
      			Details: err,
      		})
      		return
      	}
      
      	// Возвращаем успешный ответ с URL-адресами полученных файлов
      	c.JSON(http.StatusOK, gin.H{
      		"status":  http.StatusOK,
      		"message": "Files received successfully",
      		"data":    links, // URL-адреса полученных файлов
      	})
      }
      вот только не придумал что делать с ссылками - если знаете
      вот только не придумал что делать с ссылками - какие-то они некрасивые
    5. DeleteOne

      // DeleteOne обработчик для удаления одного объекта из бакета Minio по его идентификатору.
      func (h *Handler) DeleteOne(c *gin.Context) {
      	objectID := c.Param("objectID")
      
      	if err := h.minioService.DeleteOne(objectID); err != nil {
      		c.JSON(http.StatusInternalServerError, errors.ErrorResponse{
      			Status:  http.StatusInternalServerError,
      			Error:   "Cannot delete the object",
      			Details: err,
      		})
      		return
      	}
      
      	c.JSON(http.StatusOK, responses.SuccessResponse{
      		Status:  http.StatusOK,
      		Message: "File deleted successfully",
      	})
      }
      удаление одного файла в базе
      удаление одного файла в базе
    6. DeleteMany

      // DeleteMany обработчик для удаления нескольких объектов из бакета Minio по их идентификаторам.
      func (h *Handler) DeleteMany(c *gin.Context) {
      	// Объявление переменной для хранения получаемых идентификаторов объектов
      	var objectIDs dto.ObjectIdsDto
      
      	// Привязка JSON данных из запроса к переменной objectIDs
      	if err := c.BindJSON(&objectIDs); err != nil {
      		// Если привязка данных не удалась, возвращаем ошибку с соответствующим статусом и сообщением
      		c.JSON(http.StatusBadRequest, errors.ErrorResponse{
      			Status:  http.StatusBadRequest,
      			Error:   "Invalid request body", // Сообщение об ошибке в запросе
      			Details: err,                    // Детали ошибки
      		})
      		return
      	}
      
      	// Используем сервис MinIO для удаления объектов по их идентификаторам
      	if err := h.minioService.DeleteMany(objectIDs.ObjectIDs); err != nil {
      		// Если произошла ошибка при удалении объектов, возвращаем ошибку с соответствующим статусом и сообщением
      		c.JSON(http.StatusInternalServerError, errors.ErrorResponse{
      			Status:  http.StatusInternalServerError,
      			Error:   "Cannot delete many objects", // Сообщение об ошибке удаления объектов
      			Details: err,                          // Детали ошибки
      		})
      		return
      	}
      
      	// Возвращаем успешный ответ, если объекты успешно удалены
      	c.JSON(http.StatusOK, responses.SuccessResponse{
      		Status:  http.StatusOK,
      		Message: "Files deleted successfully", // Сообщение об успешном удалении файлов
      	})
      }
      
      удаление многих объект
      удаление многих объект
  6. Main file

    Сейчас надо добавить регистрацию роутов в main.go файл и теперь можно запускать проект:

    package main
    
    import (
    	"github.com/gin-gonic/gin"
    	"log"
    	"minio-gin-crud/internal/common/config"
    	"minio-gin-crud/internal/handler"
    	"minio-gin-crud/pkg/minio"
    )
    
    func main() {
    	// Загрузка конфигурации из файла .env
    	config.LoadConfig()
    
    	// Инициализация соединения с Minio
    	minioClient := minio.NewMinioClient()
    	err := minioClient.InitMinio()
    	if err != nil {
    		log.Fatalf("Ошибка инициализации Minio: %v", err)
    	}
    
    	_, s := handler.NewHandler(
    		minioClient,
    	)
    
    	// Инициализация маршрутизатора Gin
    	router := gin.Default()
    
    	s.RegisterRoutes(router)
    
    	// Запуск сервера Gin
    	port := config.AppConfig.Port // Мы берем порт из конфига
    	err = router.Run(":" + port)
    	if err != nil {
    		log.Fatalf("Ошибка запуска сервера Gin: %v", err)
    	}
    }
    

  7. Запуск и тестирование

    После запуска станет доступно 6 роутов:

    • POST http://localhost:8080/files - createOne

    • POST http://localhost:8080/files/many - createMany

    • GET http://localhost:8080/files/:objectId - getOne

    • GET http://localhost:8080/files/many - getMany {objectIDs: []string}

    • DELETE http://localhost:8080/files/:objectId - deleteOne

    • DELETE http://localhost:8080/files/many - deleteMany {objectIDs: []string}

  8. Добавим .env файл, в который добавим переменные окружения

    MINIO_ENDPOINT=localhost:9000
    MINIO_ROOT_USER=root
    MINIO_ROOT_PASSWORD=zuf+tfteSlswRu7BJ86wekitnifILbZam1KYY3TG
    MINIO_BUCKET_NAME=test-bucket
    MINIO_USE_SSL=false
    FILE_TIME_EXPIRATION=24 # в часах
    
    PORT=8080

Все - наслаждайтесь своим проектом!

Полный код будет в моем GitHub. Буду рад если оцените проект и статью звездочкой на GitHub! Оказывается на написание подобных статей уходит большое количество мыслетоплива

Если будут вопросы - вы знаете где меня искать:

Me:
-- telegram
-- telegram channel
-- GitHub

Tags:
Hubs:
Total votes 11: ↑1 and ↓10-9
Comments10

Articles