package http import ( "fmt" "io" "mime/multipart" "net/http" "strconv" "time" "github.com/NYTimes/gziphandler" "github.com/influxdata/httprouter" "github.com/influxdata/influxdb/v2" "github.com/influxdata/influxdb/v2/authorizer" "github.com/influxdata/influxdb/v2/kit/platform/errors" "github.com/influxdata/influxdb/v2/kit/tracing" "go.uber.org/zap" ) // BackupBackend is all services and associated parameters required to construct the BackupHandler. type BackupBackend struct { Logger *zap.Logger errors.HTTPErrorHandler BackupService influxdb.BackupService SqlBackupRestoreService influxdb.SqlBackupRestoreService BucketManifestWriter influxdb.BucketManifestWriter } // NewBackupBackend returns a new instance of BackupBackend. func NewBackupBackend(b *APIBackend) *BackupBackend { return &BackupBackend{ Logger: b.Logger.With(zap.String("handler", "backup")), HTTPErrorHandler: b.HTTPErrorHandler, BackupService: b.BackupService, SqlBackupRestoreService: b.SqlBackupRestoreService, BucketManifestWriter: b.BucketManifestWriter, } } // BackupHandler is http handler for backup service. type BackupHandler struct { *httprouter.Router errors.HTTPErrorHandler Logger *zap.Logger BackupService influxdb.BackupService SqlBackupRestoreService influxdb.SqlBackupRestoreService BucketManifestWriter influxdb.BucketManifestWriter } const ( prefixBackup = "/api/v2/backup" backupKVStorePath = prefixBackup + "/kv" backupShardPath = prefixBackup + "/shards/:shardID" backupMetadataPath = prefixBackup + "/metadata" ) // NewBackupHandler creates a new handler at /api/v2/backup to receive backup requests. func NewBackupHandler(b *BackupBackend) *BackupHandler { h := &BackupHandler{ HTTPErrorHandler: b.HTTPErrorHandler, Router: NewRouter(b.HTTPErrorHandler), Logger: b.Logger, BackupService: b.BackupService, SqlBackupRestoreService: b.SqlBackupRestoreService, BucketManifestWriter: b.BucketManifestWriter, } h.HandlerFunc(http.MethodGet, backupKVStorePath, h.handleBackupKVStore) // Deprecated h.Handler(http.MethodGet, backupShardPath, gziphandler.GzipHandler(http.HandlerFunc(h.handleBackupShard))) h.Handler(http.MethodGet, backupMetadataPath, gziphandler.GzipHandler(h.requireOperPermissions(http.HandlerFunc(h.handleBackupMetadata)))) return h } // requireOperPermissions returns an "unauthorized" response for requests that do not have OperPermissions. // This is needed for the handleBackupMetadata handler, which sets a header prior to // accessing any methods on the BackupService which would also return an "authorized" response. func (h *BackupHandler) requireOperPermissions(next http.Handler) http.HandlerFunc { fn := func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() if err := authorizer.IsAllowedAll(ctx, influxdb.OperPermissions()); err != nil { h.HandleHTTPError(ctx, err, w) return } next.ServeHTTP(w, r) } return http.HandlerFunc(fn) } func (h *BackupHandler) handleBackupKVStore(w http.ResponseWriter, r *http.Request) { span, r := tracing.ExtractFromHTTPRequest(r, "BackupHandler.handleBackupKVStore") defer span.Finish() ctx := r.Context() if err := h.BackupService.BackupKVStore(ctx, w); err != nil { h.HandleHTTPError(ctx, err, w) return } } func (h *BackupHandler) handleBackupShard(w http.ResponseWriter, r *http.Request) { span, r := tracing.ExtractFromHTTPRequest(r, "BackupHandler.handleBackupShard") defer span.Finish() ctx := r.Context() params := httprouter.ParamsFromContext(ctx) shardID, err := strconv.ParseUint(params.ByName("shardID"), 10, 64) if err != nil { h.HandleHTTPError(ctx, err, w) return } var since time.Time if s := r.URL.Query().Get("since"); s != "" { if since, err = time.ParseInLocation(time.RFC3339, s, time.UTC); err != nil { h.HandleHTTPError(ctx, err, w) return } } if err := h.BackupService.BackupShard(ctx, w, shardID, since); err != nil { h.HandleHTTPError(ctx, err, w) return } } func (h *BackupHandler) handleBackupMetadata(w http.ResponseWriter, r *http.Request) { span, r := tracing.ExtractFromHTTPRequest(r, "BackupHandler.handleBackupMetadata") defer span.Finish() ctx := r.Context() // Lock the sqlite and bolt databases prior to writing the response to prevent // data inconsistencies. h.BackupService.RLockKVStore() defer h.BackupService.RUnlockKVStore() h.SqlBackupRestoreService.RLockSqlStore() defer h.SqlBackupRestoreService.RUnlockSqlStore() dataWriter := multipart.NewWriter(w) w.Header().Set("Content-Type", "multipart/mixed; boundary="+dataWriter.Boundary()) parts := []struct { contentType string contentDisposition string writeFn func(io.Writer) error }{ { "application/octet-stream", fmt.Sprintf("attachment; name=%q", "kv"), func(fw io.Writer) error { return h.BackupService.BackupKVStore(ctx, fw) }, }, { "application/octet-stream", fmt.Sprintf("attachment; name=%q", "sql"), func(fw io.Writer) error { return h.SqlBackupRestoreService.BackupSqlStore(ctx, fw) }, }, { "application/json; charset=utf-8", fmt.Sprintf("attachment; name=%q", "buckets"), func(fw io.Writer) error { return h.BucketManifestWriter.WriteManifest(ctx, fw) }, }, } for _, p := range parts { pw, err := dataWriter.CreatePart(map[string][]string{ "Content-Type": {p.contentType}, "Content-Disposition": {p.contentDisposition}, }) if err != nil { h.HandleHTTPError(ctx, err, w) return } if err := p.writeFn(pw); err != nil { h.HandleHTTPError(ctx, err, w) return } } if err := dataWriter.Close(); err != nil { h.HandleHTTPError(ctx, err, w) return } }