2020-05-03 19:57:56 +08:00
package repo
2020-05-03 19:56:59 +08:00
import (
2021-08-09 15:35:13 +02:00
"encoding/hex"
2020-05-03 19:56:59 +08:00
"encoding/json"
2021-08-21 13:43:51 +02:00
"errors"
2020-05-03 19:56:59 +08:00
"fmt"
"io"
"io/ioutil"
"log"
2021-09-24 22:53:45 +02:00
"math/rand"
2020-05-03 19:56:59 +08:00
"net/http"
"os"
"path/filepath"
2020-05-03 19:57:56 +08:00
"regexp"
2021-07-16 21:47:35 +02:00
"runtime"
2021-09-24 22:53:45 +02:00
"strconv"
2020-05-03 19:56:59 +08:00
"strings"
2022-12-02 21:08:15 +01:00
"sync"
2021-08-21 13:43:51 +02:00
"syscall"
2020-05-03 19:56:59 +08:00
"time"
2021-08-27 18:00:50 +02:00
"github.com/minio/sha256-simd"
2020-05-03 19:56:59 +08:00
"github.com/miolini/datacounter"
2020-05-04 01:28:13 +08:00
"github.com/restic/rest-server/quota"
2020-05-03 19:56:59 +08:00
)
2020-05-03 19:57:56 +08:00
// Options are options for the Handler accepted by New
type Options struct {
2021-08-09 15:40:50 +02:00
AppendOnly bool // if set, delete actions are not allowed
Debug bool
DirMode os . FileMode
FileMode os . FileMode
NoVerifyUpload bool
2020-05-03 19:57:56 +08:00
2020-05-31 21:39:27 +08:00
// If set, we will panic when an internal server error happens. This
// makes it easier to debug such errors.
PanicOnError bool
2020-05-03 19:57:56 +08:00
BlobMetricFunc BlobMetricFunc
2020-05-04 01:28:13 +08:00
QuotaManager * quota . Manager
2023-05-13 21:50:39 +02:00
FsyncWarning * sync . Once
2020-05-03 19:56:59 +08:00
}
2020-05-03 19:57:56 +08:00
// DefaultDirMode is the file mode used for directory creation if not
// overridden in the Options
const DefaultDirMode os . FileMode = 0700
2020-05-03 19:56:59 +08:00
2020-05-03 19:57:56 +08:00
// DefaultFileMode is the file mode used for file creation if not
// overridden in the Options
const DefaultFileMode os . FileMode = 0600
2020-05-03 19:56:59 +08:00
2020-05-03 19:57:56 +08:00
// New creates a new Handler for a single Restic backup repo.
// path is the full filesystem path to this repo directory.
// opt is a set of options.
func New ( path string , opt Options ) ( * Handler , error ) {
if path == "" {
return nil , fmt . Errorf ( "path is required" )
}
if opt . DirMode == 0 {
opt . DirMode = DefaultDirMode
}
if opt . FileMode == 0 {
opt . FileMode = DefaultFileMode
}
h := Handler {
path : path ,
opt : opt ,
}
return & h , nil
2020-05-03 19:56:59 +08:00
}
2020-05-03 19:57:56 +08:00
// Handler handles all REST API requests for a single Restic backup repo
2020-05-04 01:28:13 +08:00
// Spec: https://restic.readthedocs.io/en/latest/100_references.html#rest-backend
2020-05-03 19:57:56 +08:00
type Handler struct {
path string // filesystem path of repo
opt Options
}
2020-05-03 19:56:59 +08:00
2020-05-03 19:57:56 +08:00
// httpDefaultError write a HTTP error with the default description
func httpDefaultError ( w http . ResponseWriter , code int ) {
http . Error ( w , http . StatusText ( code ) , code )
}
2020-05-03 19:56:59 +08:00
2020-05-03 19:57:56 +08:00
// httpMethodNotAllowed writes a 405 Method Not Allowed HTTP error with
// the required Allow header listing the methods that are allowed.
func httpMethodNotAllowed ( w http . ResponseWriter , allowed [ ] string ) {
w . Header ( ) . Set ( "Allow" , strings . Join ( allowed , ", " ) )
httpDefaultError ( w , http . StatusMethodNotAllowed )
2020-05-03 19:56:59 +08:00
}
2021-09-06 22:13:33 +02:00
// errFileContentDoesntMatchHash is the error raised when the file content hash
// doesn't match the hash provided in the URL
var errFileContentDoesntMatchHash = errors . New ( "file content does not match hash" )
2020-05-03 19:57:56 +08:00
// BlobPathRE matches valid blob URI paths with optional object IDs
var BlobPathRE = regexp . MustCompile ( ` ^/(data|index|keys|locks|snapshots)/([0-9a-f] { 64})?$ ` )
2020-05-03 19:56:59 +08:00
2020-05-03 19:57:56 +08:00
// ObjectTypes are subdirs that are used for object storage
var ObjectTypes = [ ] string { "data" , "index" , "keys" , "locks" , "snapshots" }
// FileTypes are files stored directly under the repo direct that are accessible
// through a request
var FileTypes = [ ] string { "config" }
2020-05-03 19:56:59 +08:00
2020-05-03 19:57:56 +08:00
func isHashed ( objectType string ) bool {
return objectType == "data"
2020-05-03 19:56:59 +08:00
}
2021-08-09 10:43:33 +02:00
// BlobOperation describe the current blob operation in the BlobMetricFunc callback.
2020-05-03 19:57:56 +08:00
type BlobOperation byte
2020-05-03 19:56:59 +08:00
2021-08-09 10:43:33 +02:00
// Define all valid operations.
2020-05-03 19:57:56 +08:00
const (
BlobRead = 'R' // A blob has been read
BlobWrite = 'W' // A blob has been written
BlobDelete = 'D' // A blob has been deleted
)
2020-05-03 19:56:59 +08:00
2020-05-03 19:57:56 +08:00
// BlobMetricFunc is the callback signature for blob metrics. Such a callback
// can be passed in the Options to keep track of various metrics.
// objectType: one of ObjectTypes
// operation: one of the BlobOperations above
// nBytes: the number of bytes affected, or 0 if not relevant
2020-05-04 01:28:13 +08:00
// TODO: Perhaps add http.Request for the username so that this can be cached?
2020-05-03 19:57:56 +08:00
type BlobMetricFunc func ( objectType string , operation BlobOperation , nBytes uint64 )
// ServeHTTP performs strict matching on the repo part of the URL path and
// dispatches the request to the appropriate handler.
func ( h * Handler ) ServeHTTP ( w http . ResponseWriter , r * http . Request ) {
urlPath := r . URL . Path
if urlPath == "/" {
// TODO: add HEAD and GET
switch r . Method {
case "POST" :
h . createRepo ( w , r )
default :
httpMethodNotAllowed ( w , [ ] string { "POST" } )
}
return
} else if urlPath == "/config" {
switch r . Method {
case "HEAD" :
h . checkConfig ( w , r )
case "GET" :
h . getConfig ( w , r )
case "POST" :
h . saveConfig ( w , r )
case "DELETE" :
h . deleteConfig ( w , r )
default :
httpMethodNotAllowed ( w , [ ] string { "HEAD" , "GET" , "POST" , "DELETE" } )
}
return
} else if objectType , objectID := h . getObject ( urlPath ) ; objectType != "" {
if objectID == "" {
// TODO: add HEAD
switch r . Method {
case "GET" :
h . listBlobs ( w , r )
default :
httpMethodNotAllowed ( w , [ ] string { "GET" } )
}
2021-08-09 10:43:33 +02:00
2020-05-03 19:57:56 +08:00
return
}
2021-08-09 10:43:33 +02:00
switch r . Method {
case "HEAD" :
h . checkBlob ( w , r )
case "GET" :
h . getBlob ( w , r )
case "POST" :
h . saveBlob ( w , r )
case "DELETE" :
h . deleteBlob ( w , r )
default :
httpMethodNotAllowed ( w , [ ] string { "HEAD" , "GET" , "POST" , "DELETE" } )
}
return
2020-05-03 19:56:59 +08:00
}
2020-05-03 19:57:56 +08:00
httpDefaultError ( w , http . StatusNotFound )
2020-05-03 19:56:59 +08:00
}
2020-05-03 19:57:56 +08:00
// getObject parses the URL path and returns the objectType and objectID,
// if any. The objectID is optional.
func ( h * Handler ) getObject ( urlPath string ) ( objectType , objectID string ) {
2020-05-31 21:36:39 +08:00
m := BlobPathRE . FindStringSubmatch ( urlPath )
if len ( m ) == 0 {
return "" , "" // no match
}
if len ( m ) == 2 || m [ 2 ] == "" {
return m [ 1 ] , "" // no objectID
2020-05-03 19:56:59 +08:00
}
2020-05-31 21:36:39 +08:00
return m [ 1 ] , m [ 2 ]
2020-05-03 19:57:56 +08:00
}
2020-05-03 19:56:59 +08:00
2020-05-03 19:57:56 +08:00
// getSubPath returns the path for a file or subdir in the root of the repo.
func ( h * Handler ) getSubPath ( name string ) string {
return filepath . Join ( h . path , name )
}
2020-05-03 19:56:59 +08:00
2020-05-03 19:57:56 +08:00
// getObjectPath returns the path for an object file in the repo.
// The passed in objectType and objectID must be valid due to earlier validation
func ( h * Handler ) getObjectPath ( objectType , objectID string ) string {
// If we hit an error, this is a programming error, because all of these
// must have been validated before. We still check them here as a safeguard.
if objectType == "" || objectID == "" {
panic ( "invalid objectType or objectID" )
}
if isHashed ( objectType ) {
if len ( objectID ) < 2 {
// Should never happen, because BlobPathRE checked this
panic ( "getObjectPath: objectID shorter than 2 chars" )
}
// Added another dir in between with the first two characters of the hash
return filepath . Join ( h . path , objectType , objectID [ : 2 ] , objectID )
2020-05-03 19:56:59 +08:00
}
2020-05-03 19:57:56 +08:00
return filepath . Join ( h . path , objectType , objectID )
2020-05-03 19:56:59 +08:00
}
2020-05-03 19:57:56 +08:00
// sendMetric calls op.BlobMetricFunc if set. See its signature for details.
func ( h * Handler ) sendMetric ( objectType string , operation BlobOperation , nBytes uint64 ) {
if f := h . opt . BlobMetricFunc ; f != nil {
f ( objectType , operation , nBytes )
2020-05-03 19:56:59 +08:00
}
}
2020-05-03 19:57:56 +08:00
// needSize tells you if we need the file size for metrics of quota accounting
func ( h * Handler ) needSize ( ) bool {
2020-05-04 01:28:13 +08:00
return h . opt . BlobMetricFunc != nil || h . opt . QuotaManager != nil
2020-05-03 19:56:59 +08:00
}
2020-05-03 19:57:56 +08:00
// incrementRepoSpaceUsage increments the repo space usage if quota are enabled
func ( h * Handler ) incrementRepoSpaceUsage ( by int64 ) {
2020-05-04 01:28:13 +08:00
if h . opt . QuotaManager != nil {
h . opt . QuotaManager . IncUsage ( by )
2020-05-03 19:56:59 +08:00
}
}
2020-05-03 19:57:56 +08:00
// wrapFileWriter wraps the file writer if repo quota are enabled, and returns it
// as is if not.
// If an error occurs, it returns both an error and the appropriate HTTP error code.
func ( h * Handler ) wrapFileWriter ( r * http . Request , w io . Writer ) ( io . Writer , int , error ) {
2020-05-04 01:28:13 +08:00
if h . opt . QuotaManager == nil {
return w , 0 , nil // unmodified
2020-05-03 19:57:56 +08:00
}
2020-05-04 01:28:13 +08:00
return h . opt . QuotaManager . WrapWriter ( r , w )
2020-05-03 19:56:59 +08:00
}
2020-05-03 19:57:56 +08:00
// checkConfig checks whether a configuration exists.
func ( h * Handler ) checkConfig ( w http . ResponseWriter , r * http . Request ) {
if h . opt . Debug {
log . Println ( "checkConfig()" )
2020-05-03 19:56:59 +08:00
}
2020-05-03 19:57:56 +08:00
cfg := h . getSubPath ( "config" )
2020-05-03 19:56:59 +08:00
st , err := os . Stat ( cfg )
if err != nil {
2022-08-29 22:26:34 +02:00
h . fileAccessError ( w , err )
2020-05-03 19:56:59 +08:00
return
}
w . Header ( ) . Add ( "Content-Length" , fmt . Sprint ( st . Size ( ) ) )
}
2020-05-03 19:57:56 +08:00
// getConfig allows for a config to be retrieved.
func ( h * Handler ) getConfig ( w http . ResponseWriter , r * http . Request ) {
if h . opt . Debug {
log . Println ( "getConfig()" )
2020-05-03 19:56:59 +08:00
}
2020-05-03 19:57:56 +08:00
cfg := h . getSubPath ( "config" )
2020-05-03 19:56:59 +08:00
bytes , err := ioutil . ReadFile ( cfg )
if err != nil {
2022-08-29 22:26:34 +02:00
h . fileAccessError ( w , err )
2020-05-03 19:56:59 +08:00
return
}
_ , _ = w . Write ( bytes )
}
2020-05-03 19:57:56 +08:00
// saveConfig allows for a config to be saved.
func ( h * Handler ) saveConfig ( w http . ResponseWriter , r * http . Request ) {
if h . opt . Debug {
log . Println ( "saveConfig()" )
2020-05-03 19:56:59 +08:00
}
2020-05-03 19:57:56 +08:00
cfg := h . getSubPath ( "config" )
2020-05-03 19:56:59 +08:00
2020-05-03 19:57:56 +08:00
f , err := os . OpenFile ( cfg , os . O_CREATE | os . O_WRONLY | os . O_EXCL , h . opt . FileMode )
2020-05-03 19:56:59 +08:00
if err != nil && os . IsExist ( err ) {
2020-05-03 19:57:56 +08:00
if h . opt . Debug {
2020-05-03 19:56:59 +08:00
log . Print ( err )
}
2020-05-03 19:57:56 +08:00
httpDefaultError ( w , http . StatusForbidden )
2020-05-03 19:56:59 +08:00
return
}
_ , err = io . Copy ( f , r . Body )
if err != nil {
2020-05-31 21:39:27 +08:00
h . internalServerError ( w , err )
2020-05-03 19:56:59 +08:00
return
}
err = f . Close ( )
if err != nil {
2020-05-31 21:39:27 +08:00
h . internalServerError ( w , err )
2020-05-03 19:56:59 +08:00
return
}
_ = r . Body . Close ( )
}
2020-05-03 19:57:56 +08:00
// deleteConfig removes a config.
func ( h * Handler ) deleteConfig ( w http . ResponseWriter , r * http . Request ) {
if h . opt . Debug {
log . Println ( "deleteConfig()" )
2020-05-03 19:56:59 +08:00
}
2020-05-03 19:57:56 +08:00
if h . opt . AppendOnly {
httpDefaultError ( w , http . StatusForbidden )
2020-05-03 19:56:59 +08:00
return
}
2020-05-03 19:57:56 +08:00
cfg := h . getSubPath ( "config" )
2020-05-03 19:56:59 +08:00
if err := os . Remove ( cfg ) ; err != nil {
2022-09-02 23:22:42 +02:00
// ignore not exist errors to make deleting idempotent, which is
// necessary to properly handle request retries
if ! errors . Is ( err , os . ErrNotExist ) {
h . fileAccessError ( w , err )
}
2020-05-03 19:56:59 +08:00
return
}
}
const (
mimeTypeAPIV1 = "application/vnd.x.restic.rest.v1"
mimeTypeAPIV2 = "application/vnd.x.restic.rest.v2"
)
2020-05-03 19:57:56 +08:00
// listBlobs lists all blobs of a given type in an arbitrary order.
func ( h * Handler ) listBlobs ( w http . ResponseWriter , r * http . Request ) {
if h . opt . Debug {
log . Println ( "listBlobs()" )
2020-05-03 19:56:59 +08:00
}
switch r . Header . Get ( "Accept" ) {
case mimeTypeAPIV2 :
2020-05-03 19:57:56 +08:00
h . listBlobsV2 ( w , r )
2020-05-03 19:56:59 +08:00
default :
2020-05-03 19:57:56 +08:00
h . listBlobsV1 ( w , r )
2020-05-03 19:56:59 +08:00
}
}
2020-05-03 19:57:56 +08:00
// listBlobsV1 lists all blobs of a given type in an arbitrary order.
// TODO: unify listBlobsV1 and listBlobsV2
func ( h * Handler ) listBlobsV1 ( w http . ResponseWriter , r * http . Request ) {
if h . opt . Debug {
log . Println ( "listBlobsV1()" )
2020-05-03 19:56:59 +08:00
}
2020-05-03 19:57:56 +08:00
objectType , _ := h . getObject ( r . URL . Path )
if objectType == "" {
2020-05-31 21:39:27 +08:00
h . internalServerError ( w , fmt . Errorf (
"cannot determine object type: %s" , r . URL . Path ) )
2020-05-03 19:56:59 +08:00
return
}
2020-05-03 19:57:56 +08:00
path := h . getSubPath ( objectType )
2020-05-03 19:56:59 +08:00
items , err := ioutil . ReadDir ( path )
if err != nil {
2022-08-29 22:26:34 +02:00
h . fileAccessError ( w , err )
2020-05-03 19:56:59 +08:00
return
}
2023-06-16 21:52:50 +02:00
names := [ ] string { }
2020-05-03 19:56:59 +08:00
for _ , i := range items {
2020-05-03 19:57:56 +08:00
if isHashed ( objectType ) {
2023-04-08 19:57:20 +02:00
if ! i . IsDir ( ) {
// ignore files in intermediate directories
continue
}
2020-05-03 19:56:59 +08:00
subpath := filepath . Join ( path , i . Name ( ) )
var subitems [ ] os . FileInfo
subitems , err = ioutil . ReadDir ( subpath )
if err != nil {
2022-08-29 22:26:34 +02:00
h . fileAccessError ( w , err )
2020-05-03 19:56:59 +08:00
return
}
for _ , f := range subitems {
names = append ( names , f . Name ( ) )
}
} else {
names = append ( names , i . Name ( ) )
}
}
data , err := json . Marshal ( names )
if err != nil {
2021-01-04 14:33:31 +08:00
h . internalServerError ( w , err )
2020-05-03 19:56:59 +08:00
return
}
w . Header ( ) . Set ( "Content-Type" , mimeTypeAPIV1 )
_ , _ = w . Write ( data )
}
// Blob represents a single blob, its name and its size.
type Blob struct {
Name string ` json:"name" `
Size int64 ` json:"size" `
}
2020-05-03 19:57:56 +08:00
// listBlobsV2 lists all blobs of a given type, together with their sizes, in an arbitrary order.
// TODO: unify listBlobsV1 and listBlobsV2
func ( h * Handler ) listBlobsV2 ( w http . ResponseWriter , r * http . Request ) {
if h . opt . Debug {
log . Println ( "listBlobsV2()" )
2020-05-03 19:56:59 +08:00
}
2020-05-03 19:57:56 +08:00
objectType , _ := h . getObject ( r . URL . Path )
if objectType == "" {
2020-05-31 21:39:27 +08:00
h . internalServerError ( w , fmt . Errorf (
"cannot determine object type: %s" , r . URL . Path ) )
2020-05-03 19:56:59 +08:00
return
}
2020-05-03 19:57:56 +08:00
path := h . getSubPath ( objectType )
2020-05-03 19:56:59 +08:00
items , err := ioutil . ReadDir ( path )
if err != nil {
2022-08-29 22:26:34 +02:00
h . fileAccessError ( w , err )
2020-05-03 19:56:59 +08:00
return
}
2023-06-16 21:52:50 +02:00
blobs := [ ] Blob { }
2020-05-03 19:56:59 +08:00
for _ , i := range items {
2020-05-03 19:57:56 +08:00
if isHashed ( objectType ) {
2023-04-08 19:57:20 +02:00
if ! i . IsDir ( ) {
// ignore files in intermediate directories
continue
}
2020-05-03 19:56:59 +08:00
subpath := filepath . Join ( path , i . Name ( ) )
var subitems [ ] os . FileInfo
subitems , err = ioutil . ReadDir ( subpath )
if err != nil {
2022-08-29 22:26:34 +02:00
h . fileAccessError ( w , err )
2020-05-03 19:56:59 +08:00
return
}
for _ , f := range subitems {
blobs = append ( blobs , Blob { Name : f . Name ( ) , Size : f . Size ( ) } )
}
} else {
blobs = append ( blobs , Blob { Name : i . Name ( ) , Size : i . Size ( ) } )
}
}
data , err := json . Marshal ( blobs )
if err != nil {
2020-05-31 21:39:27 +08:00
h . internalServerError ( w , err )
2020-05-03 19:56:59 +08:00
return
}
w . Header ( ) . Set ( "Content-Type" , mimeTypeAPIV2 )
_ , _ = w . Write ( data )
}
2020-05-03 19:57:56 +08:00
// checkBlob tests whether a blob exists.
func ( h * Handler ) checkBlob ( w http . ResponseWriter , r * http . Request ) {
if h . opt . Debug {
log . Println ( "checkBlob()" )
2020-05-03 19:56:59 +08:00
}
2020-05-03 19:57:56 +08:00
objectType , objectID := h . getObject ( r . URL . Path )
2020-05-31 21:39:27 +08:00
if objectType == "" || objectID == "" {
h . internalServerError ( w , fmt . Errorf (
"cannot determine object type or id: %s" , r . URL . Path ) )
2020-05-03 19:56:59 +08:00
return
}
2020-05-03 19:57:56 +08:00
path := h . getObjectPath ( objectType , objectID )
2020-05-03 19:56:59 +08:00
st , err := os . Stat ( path )
if err != nil {
2022-08-29 22:26:34 +02:00
h . fileAccessError ( w , err )
2020-05-03 19:56:59 +08:00
return
}
w . Header ( ) . Add ( "Content-Length" , fmt . Sprint ( st . Size ( ) ) )
}
2020-05-03 19:57:56 +08:00
// getBlob retrieves a blob from the repository.
func ( h * Handler ) getBlob ( w http . ResponseWriter , r * http . Request ) {
if h . opt . Debug {
log . Println ( "getBlob()" )
2020-05-03 19:56:59 +08:00
}
2020-05-03 19:57:56 +08:00
objectType , objectID := h . getObject ( r . URL . Path )
2020-05-31 21:39:27 +08:00
if objectType == "" || objectID == "" {
h . internalServerError ( w , fmt . Errorf (
"cannot determine object type or id: %s" , r . URL . Path ) )
2020-05-03 19:56:59 +08:00
return
}
2020-05-03 19:57:56 +08:00
path := h . getObjectPath ( objectType , objectID )
2020-05-03 19:56:59 +08:00
file , err := os . Open ( path )
if err != nil {
2022-08-29 22:26:34 +02:00
h . fileAccessError ( w , err )
2020-05-03 19:56:59 +08:00
return
}
wc := datacounter . NewResponseWriterCounter ( w )
http . ServeContent ( wc , r , "" , time . Unix ( 0 , 0 ) , file )
if err = file . Close ( ) ; err != nil {
2020-05-31 21:39:27 +08:00
h . internalServerError ( w , err )
2020-05-03 19:56:59 +08:00
return
}
2020-05-03 19:57:56 +08:00
h . sendMetric ( objectType , BlobRead , wc . Count ( ) )
2020-05-03 19:56:59 +08:00
}
2020-05-03 19:57:56 +08:00
// saveBlob saves a blob to the repository.
func ( h * Handler ) saveBlob ( w http . ResponseWriter , r * http . Request ) {
if h . opt . Debug {
log . Println ( "saveBlob()" )
2020-05-03 19:56:59 +08:00
}
2020-05-03 19:57:56 +08:00
objectType , objectID := h . getObject ( r . URL . Path )
2020-05-31 21:39:27 +08:00
if objectType == "" || objectID == "" {
h . internalServerError ( w , fmt . Errorf (
"cannot determine object type or id: %s" , r . URL . Path ) )
2020-05-03 19:56:59 +08:00
return
}
2020-05-03 19:57:56 +08:00
path := h . getObjectPath ( objectType , objectID )
2020-05-03 19:56:59 +08:00
2021-01-29 18:32:33 +01:00
_ , err := os . Stat ( path )
if err == nil {
httpDefaultError ( w , http . StatusForbidden )
return
}
if ! os . IsNotExist ( err ) {
h . internalServerError ( w , err )
return
}
2021-09-24 22:53:45 +02:00
tmpFn := filepath . Join ( filepath . Dir ( path ) , objectID + ".rest-server-temp" )
tf , err := tempFile ( tmpFn , h . opt . FileMode )
2020-05-03 19:56:59 +08:00
if os . IsNotExist ( err ) {
// the error is caused by a missing directory, create it and retry
2020-05-03 19:57:56 +08:00
mkdirErr := os . MkdirAll ( filepath . Dir ( path ) , h . opt . DirMode )
2020-05-03 19:56:59 +08:00
if mkdirErr != nil {
log . Print ( mkdirErr )
} else {
// try again
2021-09-24 22:53:45 +02:00
tf , err = tempFile ( tmpFn , h . opt . FileMode )
2020-05-03 19:56:59 +08:00
}
}
if err != nil {
2020-05-31 21:39:27 +08:00
h . internalServerError ( w , err )
2020-05-03 19:56:59 +08:00
return
}
2020-05-03 19:57:56 +08:00
// ensure this blob does not put us over the quota size limit (if there is one)
outFile , errCode , err := h . wrapFileWriter ( r , tf )
if err != nil {
if h . opt . Debug {
log . Println ( err )
2020-05-03 19:56:59 +08:00
}
2020-05-03 19:57:56 +08:00
httpDefaultError ( w , errCode )
return
2020-05-03 19:56:59 +08:00
}
2021-08-09 15:40:50 +02:00
var written int64
2021-08-09 15:35:13 +02:00
2021-08-09 15:40:50 +02:00
if h . opt . NoVerifyUpload {
// just write the file without checking the contents
written , err = io . Copy ( outFile , r . Body )
} else {
// calculate hash for current request
hasher := sha256 . New ( )
written , err = io . Copy ( outFile , io . TeeReader ( r . Body , hasher ) )
// reject if file content doesn't match file name
if err == nil && hex . EncodeToString ( hasher . Sum ( nil ) ) != objectID {
2021-09-06 22:13:33 +02:00
err = errFileContentDoesntMatchHash
2021-08-09 15:40:50 +02:00
}
2021-08-09 15:35:13 +02:00
}
2020-05-03 19:56:59 +08:00
if err != nil {
_ = tf . Close ( )
2021-01-29 18:32:33 +01:00
_ = os . Remove ( tf . Name ( ) )
2020-05-03 19:57:56 +08:00
h . incrementRepoSpaceUsage ( - written )
if h . opt . Debug {
2020-05-03 19:56:59 +08:00
log . Print ( err )
}
2021-08-21 13:43:51 +02:00
var pathError * os . PathError
2021-09-06 22:13:33 +02:00
if errors . As ( err , & pathError ) && ( pathError . Err == syscall . ENOSPC ||
pathError . Err == syscall . EDQUOT ) {
// The error is disk-related (no space left, no quota left),
// notify the client using the correct HTTP status
2021-08-21 13:43:51 +02:00
httpDefaultError ( w , http . StatusInsufficientStorage )
2021-09-06 22:13:33 +02:00
} else if errors . Is ( err , errFileContentDoesntMatchHash ) ||
errors . Is ( err , io . ErrUnexpectedEOF ) ||
errors . Is ( err , http . ErrMissingBoundary ) ||
errors . Is ( err , http . ErrNotMultipart ) {
// The error is connection-related, send a client-side HTTP status
httpDefaultError ( w , http . StatusBadRequest )
2021-08-21 13:43:51 +02:00
} else {
2021-09-06 22:13:33 +02:00
// Otherwise we have a different internal error, reply with
// server-side HTTP status
h . internalServerError ( w , err )
2021-08-21 13:43:51 +02:00
}
2020-05-03 19:56:59 +08:00
return
}
2022-11-11 22:37:02 +01:00
syncNotSup , err := syncFile ( tf )
if err != nil {
2020-05-03 19:56:59 +08:00
_ = tf . Close ( )
2021-01-29 18:32:33 +01:00
_ = os . Remove ( tf . Name ( ) )
2020-05-03 19:57:56 +08:00
h . incrementRepoSpaceUsage ( - written )
2020-05-31 21:39:27 +08:00
h . internalServerError ( w , err )
2020-05-03 19:56:59 +08:00
return
}
if err := tf . Close ( ) ; err != nil {
2021-01-29 18:32:33 +01:00
_ = os . Remove ( tf . Name ( ) )
h . incrementRepoSpaceUsage ( - written )
h . internalServerError ( w , err )
return
}
if err := os . Rename ( tf . Name ( ) , path ) ; err != nil {
_ = os . Remove ( tf . Name ( ) )
2020-05-03 19:57:56 +08:00
h . incrementRepoSpaceUsage ( - written )
2020-05-31 21:39:27 +08:00
h . internalServerError ( w , err )
2020-05-03 19:56:59 +08:00
return
}
2023-05-08 21:40:23 +02:00
if syncNotSup {
2023-05-13 21:50:39 +02:00
h . opt . FsyncWarning . Do ( func ( ) {
2023-05-08 21:40:23 +02:00
log . Print ( "WARNING: fsync is not supported by the data storage. This can lead to data loss, if the system crashes or the storage is unexpectedly disconnected." )
} )
} else {
2022-11-11 22:37:02 +01:00
if err := syncDir ( filepath . Dir ( path ) ) ; err != nil {
// Don't call os.Remove(path) as this is prone to race conditions with parallel upload retries
h . internalServerError ( w , err )
return
}
2021-01-29 19:02:22 +01:00
}
2020-05-03 19:57:56 +08:00
h . sendMetric ( objectType , BlobWrite , uint64 ( written ) )
2020-05-03 19:56:59 +08:00
}
2021-09-24 22:53:45 +02:00
// tempFile implements a custom version of ioutil.TempFile which allows modifying the file permissions
func tempFile ( fn string , perm os . FileMode ) ( f * os . File , err error ) {
for i := 0 ; i < 10 ; i ++ {
name := fn + strconv . FormatInt ( rand . Int63 ( ) , 10 )
f , err = os . OpenFile ( name , os . O_RDWR | os . O_CREATE | os . O_EXCL , perm )
if os . IsExist ( err ) {
continue
}
break
}
return
}
2022-11-11 22:37:02 +01:00
func syncFile ( f * os . File ) ( bool , error ) {
err := f . Sync ( )
// Ignore error if filesystem does not support fsync.
syncNotSup := err != nil && ( errors . Is ( err , syscall . ENOTSUP ) || isMacENOTTY ( err ) )
if syncNotSup {
err = nil
}
return syncNotSup , err
}
2021-01-29 19:02:22 +01:00
func syncDir ( dirname string ) error {
2021-07-16 21:47:35 +02:00
if runtime . GOOS == "windows" {
// syncing a directory is not possible on windows
return nil
}
2021-01-29 19:02:22 +01:00
dir , err := os . Open ( dirname )
if err != nil {
return err
}
err = dir . Sync ( )
2022-11-11 22:37:02 +01:00
// Ignore error if filesystem does not support fsync.
if errors . Is ( err , syscall . ENOTSUP ) || errors . Is ( err , syscall . ENOENT ) || errors . Is ( err , syscall . EINVAL ) {
err = nil
}
2021-01-29 19:02:22 +01:00
if err != nil {
_ = dir . Close ( )
return err
}
return dir . Close ( )
}
2020-05-03 19:57:56 +08:00
// deleteBlob deletes a blob from the repository.
func ( h * Handler ) deleteBlob ( w http . ResponseWriter , r * http . Request ) {
if h . opt . Debug {
log . Println ( "deleteBlob()" )
2020-05-03 19:56:59 +08:00
}
2020-05-03 19:57:56 +08:00
objectType , objectID := h . getObject ( r . URL . Path )
2020-05-31 21:39:27 +08:00
if objectType == "" || objectID == "" {
h . internalServerError ( w , fmt . Errorf (
"cannot determine object type or id: %s" , r . URL . Path ) )
2020-05-03 19:56:59 +08:00
return
}
2020-05-03 19:57:56 +08:00
if h . opt . AppendOnly && objectType != "locks" {
httpDefaultError ( w , http . StatusForbidden )
2020-05-03 19:56:59 +08:00
return
}
2020-05-03 19:57:56 +08:00
path := h . getObjectPath ( objectType , objectID )
2020-05-03 19:56:59 +08:00
var size int64
2020-05-03 19:57:56 +08:00
if h . needSize ( ) {
2020-05-03 19:56:59 +08:00
stat , err := os . Stat ( path )
if err == nil {
size = stat . Size ( )
}
}
if err := os . Remove ( path ) ; err != nil {
2022-09-02 23:22:42 +02:00
// ignore not exist errors to make deleting idempotent, which is
// necessary to properly handle request retries
if ! errors . Is ( err , os . ErrNotExist ) {
h . fileAccessError ( w , err )
}
2020-05-03 19:56:59 +08:00
return
}
2020-05-03 19:57:56 +08:00
h . incrementRepoSpaceUsage ( - size )
h . sendMetric ( objectType , BlobDelete , uint64 ( size ) )
2020-05-03 19:56:59 +08:00
}
2020-05-03 19:57:56 +08:00
// createRepo creates repository directories.
func ( h * Handler ) createRepo ( w http . ResponseWriter , r * http . Request ) {
if h . opt . Debug {
log . Println ( "createRepo()" )
2020-05-03 19:56:59 +08:00
}
if r . URL . Query ( ) . Get ( "create" ) != "true" {
2020-05-03 19:57:56 +08:00
httpDefaultError ( w , http . StatusBadRequest )
2020-05-03 19:56:59 +08:00
return
}
2020-05-03 19:57:56 +08:00
log . Printf ( "Creating repository directories in %s\n" , h . path )
2020-05-03 19:56:59 +08:00
2020-05-03 19:57:56 +08:00
if err := os . MkdirAll ( h . path , h . opt . DirMode ) ; err != nil {
2020-05-31 21:39:27 +08:00
h . internalServerError ( w , err )
2020-05-03 19:56:59 +08:00
return
}
2020-05-03 19:57:56 +08:00
for _ , d := range ObjectTypes {
if err := os . Mkdir ( filepath . Join ( h . path , d ) , h . opt . DirMode ) ; err != nil && ! os . IsExist ( err ) {
2020-05-31 21:39:27 +08:00
h . internalServerError ( w , err )
2020-05-03 19:56:59 +08:00
return
}
}
for i := 0 ; i < 256 ; i ++ {
2020-05-03 19:57:56 +08:00
dirPath := filepath . Join ( h . path , "data" , fmt . Sprintf ( "%02x" , i ) )
if err := os . Mkdir ( dirPath , h . opt . DirMode ) ; err != nil && ! os . IsExist ( err ) {
2020-05-31 21:39:27 +08:00
h . internalServerError ( w , err )
2020-05-03 19:56:59 +08:00
return
}
}
}
2020-05-31 21:39:27 +08:00
2022-08-29 22:26:34 +02:00
// internalServerError is called to report an internal server error.
2020-05-31 21:39:27 +08:00
// The error message will be reported in the server logs. If PanicOnError
// is set, this will panic instead, which makes debugging easier.
func ( h * Handler ) internalServerError ( w http . ResponseWriter , err error ) {
log . Printf ( "ERROR: %v" , err )
if h . opt . PanicOnError {
panic ( fmt . Sprintf ( "internal server error: %v" , err ) )
}
httpDefaultError ( w , http . StatusInternalServerError )
}
2022-08-29 22:26:34 +02:00
// internalServerError is called to report an error that occurred while
// accessing a file. If the does not exist, the corresponding http status code
// will be returned to the client. All other errors are passed on to
// internalServerError
func ( h * Handler ) fileAccessError ( w http . ResponseWriter , err error ) {
if h . opt . Debug {
log . Print ( err )
}
if errors . Is ( err , os . ErrNotExist ) {
httpDefaultError ( w , http . StatusNotFound )
} else {
h . internalServerError ( w , err )
}
}