1
This commit is contained in:
612
internal/http/logo.go
Normal file
612
internal/http/logo.go
Normal file
@@ -0,0 +1,612 @@
|
||||
package http_router
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"git-molva.ru/Molva/molva-backend/services/api_gateway/internal/auth"
|
||||
"git-molva.ru/Molva/molva-backend/services/api_gateway/internal/auth/keycloak"
|
||||
"git-molva.ru/Molva/molva-backend/services/api_gateway/internal/cache"
|
||||
"git-molva.ru/Molva/molva-backend/services/api_gateway/internal/constants"
|
||||
"git-molva.ru/Molva/molva-backend/services/api_gateway/internal/object_storage"
|
||||
"git-molva.ru/Molva/molva-backend/services/api_gateway/internal/types"
|
||||
"github.com/aws/smithy-go/ptr"
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
// =============== FILE MANAGEMENT ===============
|
||||
|
||||
// TempFileManager управляет временными файлами с автоматической очисткой
|
||||
type TempFileManager struct {
|
||||
baseDir string
|
||||
mu sync.Mutex
|
||||
files map[string]*os.File
|
||||
}
|
||||
|
||||
// Cleanup очищает все временные файлы при завершении работы
|
||||
func (h *handler) Cleanup() {
|
||||
h.tempFileManager.CleanupAll()
|
||||
}
|
||||
|
||||
func NewTempFileManager(baseDir string) (*TempFileManager, error) {
|
||||
if baseDir == "" {
|
||||
baseDir = filepath.Join(os.TempDir(), "logo-uploads")
|
||||
}
|
||||
|
||||
if err := os.MkdirAll(baseDir, constants.TempDirPermission); err != nil {
|
||||
return nil, fmt.Errorf("failed to create temp directory: %w", err)
|
||||
}
|
||||
|
||||
return &TempFileManager{
|
||||
baseDir: baseDir,
|
||||
files: make(map[string]*os.File),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (tm *TempFileManager) CreateTempFile(pattern string) (*os.File, error) {
|
||||
tempFile, err := os.CreateTemp(tm.baseDir, pattern)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
tm.mu.Lock()
|
||||
tm.files[tempFile.Name()] = tempFile
|
||||
tm.mu.Unlock()
|
||||
|
||||
return tempFile, nil
|
||||
}
|
||||
|
||||
func (tm *TempFileManager) CleanupFile(file *os.File) error {
|
||||
if file == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
tm.mu.Lock()
|
||||
delete(tm.files, file.Name())
|
||||
tm.mu.Unlock()
|
||||
|
||||
name := file.Name()
|
||||
if err := file.Close(); err != nil {
|
||||
slog.Warn("failed to close temp file", slog.String("file", name), slog.String("error", err.Error()))
|
||||
}
|
||||
|
||||
if err := os.Remove(name); err != nil && !os.IsNotExist(err) {
|
||||
return fmt.Errorf("failed to remove temp file %s: %w", name, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (tm *TempFileManager) CleanupAll() {
|
||||
tm.mu.Lock()
|
||||
defer tm.mu.Unlock()
|
||||
|
||||
for name, file := range tm.files {
|
||||
_ = file.Close()
|
||||
_ = os.Remove(name)
|
||||
}
|
||||
|
||||
tm.files = make(map[string]*os.File)
|
||||
}
|
||||
|
||||
type LogoFile struct {
|
||||
File *os.File
|
||||
ContentType string
|
||||
Size int64
|
||||
manager *TempFileManager
|
||||
}
|
||||
|
||||
// Close закрывает файл и удаляет временный файл
|
||||
func (lf *LogoFile) Close() error {
|
||||
if lf.manager != nil {
|
||||
return lf.manager.CleanupFile(lf.File)
|
||||
}
|
||||
|
||||
if lf.File != nil {
|
||||
name := lf.File.Name()
|
||||
_ = lf.File.Close()
|
||||
|
||||
return os.Remove(name)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Reset сбрасывает позицию чтения в начало файла
|
||||
func (lf *LogoFile) Reset() error {
|
||||
_, err := lf.File.Seek(0, io.SeekStart)
|
||||
return err
|
||||
}
|
||||
|
||||
// =============== LOGO HANDLERS ===============
|
||||
|
||||
func (h *handler) getCompanyLogoHandler(w http.ResponseWriter, r *http.Request) {
|
||||
const handlerName = "getCompanyLogoHandler"
|
||||
|
||||
distributorID, companyID, token := h.extractLogoRequestData(r)
|
||||
if err := h.validateUserAccess(token, distributorID); err != nil {
|
||||
h.handleAccessError(w, err, handlerName)
|
||||
return
|
||||
}
|
||||
|
||||
logoLink, err := h.getLogoLinkWithCache(r.Context(), companyID)
|
||||
if err != nil {
|
||||
if errors.Is(err, object_storage.ErrObjectNotFound) {
|
||||
response := types.GetLogoResponse{
|
||||
Link: "",
|
||||
ExpiresIn: time.Time{},
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
|
||||
if err := json.NewEncoder(w).Encode(response); err != nil {
|
||||
h.logger.Error("error encoding response",
|
||||
slog.String("error", err.Error()),
|
||||
slog.String("handler", handlerName))
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
http.Error(w, constants.ErrInternalServerError.Error(), http.StatusInternalServerError)
|
||||
h.logger.Error("error getting logo link",
|
||||
slog.String("error", err.Error()),
|
||||
slog.String("handler", handlerName))
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
response := types.GetLogoResponse{
|
||||
Link: logoLink,
|
||||
ExpiresIn: time.Now().Add(constants.LogoLinkTTL),
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
|
||||
if err := json.NewEncoder(w).Encode(response); err != nil {
|
||||
h.logger.Error("error encoding response",
|
||||
slog.String("error", err.Error()),
|
||||
slog.String("handler", handlerName))
|
||||
}
|
||||
}
|
||||
|
||||
func (h *handler) createCompanyLogoHandler(w http.ResponseWriter, r *http.Request) {
|
||||
const handlerName = "createCompanyLogoHandler"
|
||||
|
||||
distributorID, companyID, token := h.extractLogoRequestData(r)
|
||||
|
||||
if err := h.validateUserAccess(token, distributorID); err != nil {
|
||||
h.handleAccessError(w, err, handlerName)
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.validateUserPermissions(r.Context(), distributorID, keycloak.PermissionCompany, keycloak.PermissionLevelCanEdit); err != nil {
|
||||
h.handleAccessError(w, err, handlerName)
|
||||
return
|
||||
}
|
||||
|
||||
logoFile, err := h.extractAndValidateLogoFile(r)
|
||||
if err != nil {
|
||||
h.handleUploadError(w, err, handlerName)
|
||||
return
|
||||
}
|
||||
defer logoFile.Close()
|
||||
|
||||
// Сбрасываем позицию после валидации, чтобы не потерять данные
|
||||
if err := logoFile.Reset(); err != nil {
|
||||
h.logger.Error("failed to reset file position",
|
||||
slog.String("error", err.Error()),
|
||||
slog.String("handler", handlerName))
|
||||
http.Error(w, constants.ErrInternalServerError.Error(), http.StatusInternalServerError)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// может стоит добавить таймаут записи в кэш
|
||||
err = h.objectStorageClient.PutNewObject(
|
||||
r.Context(),
|
||||
companyID,
|
||||
object_storage.LogoCategory,
|
||||
logoFile.File,
|
||||
object_storage.PutOptions{
|
||||
ContentType: logoFile.ContentType,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
if errors.Is(err, object_storage.ErrObjectAlreadyExists) {
|
||||
http.Error(w, "Logo already exists. Use PUT to update", http.StatusConflict)
|
||||
return
|
||||
}
|
||||
|
||||
http.Error(w, constants.ErrInternalServerError.Error(), http.StatusInternalServerError)
|
||||
h.logger.Error("error uploading logo",
|
||||
slog.String("error", err.Error()),
|
||||
slog.String("handler", handlerName))
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
h.refreshLogoCacheAsync(companyID)
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
}
|
||||
|
||||
func (h *handler) updateCompanyLogoHandler(w http.ResponseWriter, r *http.Request) {
|
||||
const handlerName = "updateCompanyLogoHandler"
|
||||
|
||||
distributorID, companyID, token := h.extractLogoRequestData(r)
|
||||
|
||||
if err := h.validateUserAccess(token, distributorID); err != nil {
|
||||
h.handleAccessError(w, err, handlerName)
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.validateUserPermissions(r.Context(), distributorID, keycloak.PermissionCompany, keycloak.PermissionLevelCanEdit); err != nil {
|
||||
h.handleAccessError(w, err, handlerName)
|
||||
return
|
||||
}
|
||||
|
||||
logoFile, err := h.extractAndValidateLogoFile(r)
|
||||
if err != nil {
|
||||
h.handleUploadError(w, err, handlerName)
|
||||
return
|
||||
}
|
||||
defer logoFile.Close()
|
||||
|
||||
// Сбрасываем позицию после валидации, чтобы не потерять данные
|
||||
if err := logoFile.Reset(); err != nil {
|
||||
h.logger.Error("failed to reset file position",
|
||||
slog.String("error", err.Error()),
|
||||
slog.String("handler", handlerName))
|
||||
http.Error(w, constants.ErrInternalServerError.Error(), http.StatusInternalServerError)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Обновляем в хранилище
|
||||
err = h.objectStorageClient.UpdateObject(
|
||||
r.Context(),
|
||||
companyID,
|
||||
object_storage.LogoCategory,
|
||||
logoFile.File,
|
||||
object_storage.UpdateOptions{
|
||||
ContentType: logoFile.ContentType,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
if errors.Is(err, object_storage.ErrObjectNotFound) {
|
||||
http.Error(w, "Logo not found. Use POST to create", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
http.Error(w, constants.ErrInternalServerError.Error(), http.StatusInternalServerError)
|
||||
h.logger.Error("error updating logo",
|
||||
slog.String("error", err.Error()),
|
||||
slog.String("handler", handlerName))
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
h.refreshLogoCacheAsync(companyID)
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
|
||||
func (h *handler) deleteCompanyLogoHandler(w http.ResponseWriter, r *http.Request) {
|
||||
const handlerName = "deleteCompanyLogoHandler"
|
||||
|
||||
distributorID, companyID, token := h.extractLogoRequestData(r)
|
||||
|
||||
if err := h.validateUserAccess(token, distributorID); err != nil {
|
||||
h.handleAccessError(w, err, handlerName)
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.validateUserPermissions(r.Context(), distributorID, keycloak.PermissionCompany, keycloak.PermissionLevelCanEdit); err != nil {
|
||||
h.handleAccessError(w, err, handlerName)
|
||||
return
|
||||
}
|
||||
|
||||
err := h.objectStorageClient.DeleteObject(r.Context(), companyID, object_storage.LogoCategory)
|
||||
if err != nil {
|
||||
if errors.Is(err, object_storage.ErrObjectNotFound) {
|
||||
http.Error(w, constants.ErrNotFound.Error(), http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
http.Error(w, constants.ErrInternalServerError.Error(), http.StatusInternalServerError)
|
||||
h.logger.Error("error deleting logo",
|
||||
slog.String("error", err.Error()),
|
||||
slog.String("handler", handlerName))
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Очищаем кэш
|
||||
if err := h.cacheClient.Del(r.Context(), companyID, cache.LogoValueType); err != nil {
|
||||
h.logger.Error("error deleting logo from cache",
|
||||
slog.String("error", err.Error()),
|
||||
slog.String("company_id", companyID))
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
|
||||
// =============== CORE LOGIC FUNCTIONS ===============
|
||||
|
||||
// extractAndValidateLogoFile извлекает файл из запроса и сохраняет во временный файл
|
||||
func (h *handler) extractAndValidateLogoFile(r *http.Request) (*LogoFile, error) {
|
||||
err := r.ParseMultipartForm(constants.ParseMultipartFormAllFile) //сохраняем на диск весь файл
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%w: %v", ErrMultipartParse, err)
|
||||
}
|
||||
|
||||
defer func() { _ = r.MultipartForm.RemoveAll() }()
|
||||
|
||||
file, header, err := r.FormFile("logo")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%w: %v", ErrFileNotFound, err)
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
// Проверяем размер файла
|
||||
if header.Size == 0 {
|
||||
return nil, ErrEmptyFile
|
||||
}
|
||||
|
||||
if header.Size > constants.LogoMaxSize {
|
||||
return nil, &FileTooLargeError{Size: header.Size, MaxSize: constants.LogoMaxSize}
|
||||
}
|
||||
|
||||
contentType := header.Header.Get("Content-Type")
|
||||
if contentType == "" {
|
||||
contentType = "application/octet-stream"
|
||||
}
|
||||
|
||||
// Создаем временный файл для копирования
|
||||
tempFile, err := h.tempFileManager.CreateTempFile(constants.TempLogoFilePattern)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%w: %v", ErrTempFileCreate, err)
|
||||
}
|
||||
|
||||
// Копируем содержимое во временный файл
|
||||
copiedBytes, err := io.CopyN(tempFile, file, constants.LogoMaxSize+1)
|
||||
if err != nil && err != io.EOF {
|
||||
_ = h.tempFileManager.CleanupFile(tempFile)
|
||||
return nil, fmt.Errorf("%w: failed to copy file: %v", ErrFileOperation, err)
|
||||
}
|
||||
|
||||
logoFile := &LogoFile{
|
||||
File: tempFile,
|
||||
ContentType: contentType,
|
||||
Size: copiedBytes,
|
||||
manager: h.tempFileManager,
|
||||
}
|
||||
|
||||
if err := h.validateLogoFile(logoFile); err != nil {
|
||||
_ = logoFile.Close()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return logoFile, nil
|
||||
}
|
||||
|
||||
// validateLogoFile валидирует файл изображения
|
||||
func (h *handler) validateLogoFile(logoFile *LogoFile) error {
|
||||
// Проверяем заявленный тип
|
||||
if !isValidImageType(logoFile.ContentType) {
|
||||
h.logger.Error("invalid image type",
|
||||
slog.String("image_type", logoFile.ContentType),
|
||||
slog.String("handler", "validateLogoFile"))
|
||||
|
||||
return &InvalidImageTypeError{ContentType: logoFile.ContentType}
|
||||
}
|
||||
|
||||
if err := logoFile.Reset(); err != nil {
|
||||
return fmt.Errorf("%w: failed to reset file position before validation: %v", ErrFileOperation, err)
|
||||
}
|
||||
|
||||
// Читаем первые 512 байт для определения реального типа
|
||||
buffer := make([]byte, constants.BytesToDetectContentType)
|
||||
|
||||
firstBytes, err := logoFile.File.Read(buffer)
|
||||
if err != nil && err != io.EOF {
|
||||
return fmt.Errorf("%w: failed to read file header: %v", ErrFileOperation, err)
|
||||
}
|
||||
|
||||
detectedType := http.DetectContentType(buffer[:firstBytes])
|
||||
if !isValidImageType(detectedType) {
|
||||
h.logger.Error("invalid image type after detecting",
|
||||
slog.String("image_type", detectedType),
|
||||
slog.String("handler", "validateLogoFile"))
|
||||
|
||||
return &InvalidImageTypeError{ContentType: detectedType}
|
||||
}
|
||||
|
||||
// Возвращаем позицию в начало
|
||||
if err := logoFile.Reset(); err != nil {
|
||||
return fmt.Errorf("%w: failed to reset file position: %v", ErrFileOperation, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// extractLogoRequestData извлекает данные из HTTP запроса
|
||||
func (h *handler) extractLogoRequestData(r *http.Request) (distributorID, companyID, token string) {
|
||||
vars := mux.Vars(r)
|
||||
distributorID = vars["distributor_id"]
|
||||
companyID = vars["company_id"]
|
||||
token = strings.TrimPrefix(r.Header.Get("Authorization"), "Bearer ")
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// =============== VALIDATION FUNCTIONS ===============
|
||||
|
||||
// validateUserAccess проверяет авторизацию и соответствие пользователя
|
||||
func (h *handler) validateUserAccess(token, expectedUserID string) error {
|
||||
if token == "" {
|
||||
return constants.ErrUnauthorized
|
||||
}
|
||||
|
||||
userInfo, err := h.jwtManager.GetUserInfoFromToken(token)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid token: %w", err)
|
||||
}
|
||||
|
||||
if expectedUserID != userInfo.UserId {
|
||||
return constants.ErrForbidden
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// validateUserPermissions проверяет права доступа пользователя
|
||||
func (h *handler) validateUserPermissions(ctx context.Context, userID, permission, level string) error {
|
||||
ok, err := h.authManager.CheckPermissions(ctx, auth.CheckPermissionsRequest{
|
||||
UserId: userID,
|
||||
RequiredPermission: permission,
|
||||
RequiredPermissionLevel: level,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("permission check failed: %w", err)
|
||||
}
|
||||
|
||||
if !ok {
|
||||
return constants.ErrForbidden
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// isValidImageType проверяет допустимость типа изображения
|
||||
func isValidImageType(contentType string) bool {
|
||||
contentType = strings.ToLower(strings.TrimSpace(contentType))
|
||||
|
||||
if idx := strings.Index(contentType, ";"); idx != -1 {
|
||||
contentType = contentType[:idx]
|
||||
}
|
||||
|
||||
return constants.ValidImageTypes[contentType]
|
||||
}
|
||||
|
||||
// =============== CACHE FUNCTIONS ===============
|
||||
|
||||
// refreshLogoCacheAsync асинхронно обновляет кэш логотипа
|
||||
func (h *handler) refreshLogoCacheAsync(companyID string) {
|
||||
go func() {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), constants.RefreshLogoCacheAsyncTimeout)
|
||||
defer cancel()
|
||||
|
||||
// Получаем новую ссылку на логотип
|
||||
logoLink, err := h.objectStorageClient.GetPresignedLink(ctx,
|
||||
companyID, object_storage.LogoCategory, object_storage.LinkOptions{
|
||||
TTL: ptr.Duration(constants.LogoLinkTTL),
|
||||
})
|
||||
if err != nil {
|
||||
h.logger.Warn("failed to get logo link for cache refresh",
|
||||
slog.String("error", err.Error()),
|
||||
slog.String("company_id", companyID))
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if err = h.cacheClient.Del(ctx, companyID, cache.LogoValueType); err != nil {
|
||||
h.logger.Warn("failed to delete logo from cache, it's ok if it first time creation",
|
||||
slog.String("error", err.Error()),
|
||||
slog.String("company_id", companyID))
|
||||
}
|
||||
|
||||
response := types.GetLogoResponse{
|
||||
Link: logoLink,
|
||||
ExpiresIn: time.Now().Add(constants.LogoLinkTTL),
|
||||
}
|
||||
|
||||
body, err := json.Marshal(response)
|
||||
if err != nil {
|
||||
h.logger.Warn("failed to marshal logo response for cache",
|
||||
slog.String("error", err.Error()),
|
||||
slog.String("company_id", companyID))
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.cacheClient.Set(ctx, companyID, cache.LogoValueType, string(body), constants.LogoLinkTTL); err != nil {
|
||||
h.logger.Warn("failed to update logo cache",
|
||||
slog.String("error", err.Error()),
|
||||
slog.String("company_id", companyID))
|
||||
} else {
|
||||
h.logger.Debug("logo cache refreshed successfully",
|
||||
slog.String("company_id", companyID))
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// getLogoLinkWithCache получает ссылку на логотип из кэша или хранилища
|
||||
func (h *handler) getLogoLinkWithCache(ctx context.Context, companyID string) (string, error) {
|
||||
// Проверяем кэш
|
||||
cachedData, err := h.cacheClient.Get(ctx, companyID, cache.LogoValueType)
|
||||
if err == nil {
|
||||
var response types.GetLogoResponse
|
||||
if err := json.Unmarshal([]byte(cachedData), &response); err == nil {
|
||||
return response.Link, nil
|
||||
}
|
||||
|
||||
h.logger.Warn("failed to unmarshal logo response from cache",
|
||||
slog.String("company_id", companyID))
|
||||
}
|
||||
|
||||
if !errors.Is(err, cache.ErrKeyNotFound) {
|
||||
h.logger.Warn("failed to get logo from cache",
|
||||
slog.String("error", err.Error()),
|
||||
slog.String("company_id", companyID))
|
||||
}
|
||||
|
||||
// Получаем из хранилища ссылку на логотип
|
||||
logoLink, err := h.objectStorageClient.GetPresignedLink(ctx,
|
||||
companyID, object_storage.LogoCategory, object_storage.LinkOptions{
|
||||
TTL: ptr.Duration(constants.LogoLinkTTL),
|
||||
})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
response := types.GetLogoResponse{
|
||||
Link: logoLink,
|
||||
ExpiresIn: time.Now().Add(constants.LogoLinkTTL),
|
||||
}
|
||||
|
||||
body, err := json.Marshal(response)
|
||||
if err != nil {
|
||||
h.logger.Warn("failed to marshal logo response for cache",
|
||||
slog.String("error", err.Error()),
|
||||
slog.String("company_id", companyID))
|
||||
} else {
|
||||
if err := h.cacheClient.Set(ctx, companyID, cache.LogoValueType, string(body), constants.LogoLinkTTL); err != nil {
|
||||
h.logger.Warn("failed to cache logo response",
|
||||
slog.String("error", err.Error()),
|
||||
slog.String("company_id", companyID))
|
||||
}
|
||||
}
|
||||
|
||||
return logoLink, nil
|
||||
}
|
||||
|
||||
// =============== GRACEFUL SHUTDOWN ===============
|
||||
|
||||
// SetupGracefulShutdown делает graceful shutdown)
|
||||
func (h *handler) SetupGracefulShutdown(ctx context.Context) {
|
||||
go func() {
|
||||
<-ctx.Done()
|
||||
h.logger.Info("shutting down, cleaning up temporary files")
|
||||
h.Cleanup()
|
||||
}()
|
||||
}
|
Reference in New Issue
Block a user