github.com/ethersphere/bee/v2@v2.2.0/pkg/api/dirs.go (about)

     1  // Copyright 2020 The Swarm Authors. All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  package api
     6  
     7  import (
     8  	"archive/tar"
     9  	"context"
    10  	"errors"
    11  	"fmt"
    12  	"io"
    13  	"mime"
    14  	"mime/multipart"
    15  	"net/http"
    16  	"path/filepath"
    17  	"runtime"
    18  	"strconv"
    19  	"strings"
    20  
    21  	"github.com/ethersphere/bee/v2/pkg/accesscontrol"
    22  	"github.com/ethersphere/bee/v2/pkg/file/loadsave"
    23  	"github.com/ethersphere/bee/v2/pkg/file/redundancy"
    24  	"github.com/ethersphere/bee/v2/pkg/jsonhttp"
    25  	"github.com/ethersphere/bee/v2/pkg/log"
    26  	"github.com/ethersphere/bee/v2/pkg/manifest"
    27  	"github.com/ethersphere/bee/v2/pkg/postage"
    28  	storage "github.com/ethersphere/bee/v2/pkg/storage"
    29  	storer "github.com/ethersphere/bee/v2/pkg/storer"
    30  	"github.com/ethersphere/bee/v2/pkg/swarm"
    31  	"github.com/ethersphere/bee/v2/pkg/tracing"
    32  	"github.com/opentracing/opentracing-go"
    33  	"github.com/opentracing/opentracing-go/ext"
    34  	olog "github.com/opentracing/opentracing-go/log"
    35  )
    36  
    37  var errEmptyDir = errors.New("no files in root directory")
    38  
    39  // dirUploadHandler uploads a directory supplied as a tar in an HTTP request
    40  func (s *Service) dirUploadHandler(
    41  	ctx context.Context,
    42  	logger log.Logger,
    43  	span opentracing.Span,
    44  	w http.ResponseWriter,
    45  	r *http.Request,
    46  	putter storer.PutterSession,
    47  	contentTypeString string,
    48  	encrypt bool,
    49  	tag uint64,
    50  	rLevel redundancy.Level,
    51  	act bool,
    52  	historyAddress swarm.Address,
    53  ) {
    54  	if r.Body == http.NoBody {
    55  		logger.Error(nil, "request has no body")
    56  		jsonhttp.BadRequest(w, errInvalidRequest)
    57  		return
    58  	}
    59  
    60  	// The error is ignored because the header was already validated by the caller.
    61  	mediaType, params, _ := mime.ParseMediaType(contentTypeString)
    62  
    63  	var dReader dirReader
    64  	switch mediaType {
    65  	case contentTypeTar:
    66  		dReader = &tarReader{r: tar.NewReader(r.Body), logger: s.logger}
    67  	case multiPartFormData:
    68  		dReader = &multipartReader{r: multipart.NewReader(r.Body, params["boundary"])}
    69  	default:
    70  		logger.Error(nil, "invalid content-type for directory upload")
    71  		jsonhttp.BadRequest(w, errInvalidContentType)
    72  		return
    73  	}
    74  	defer r.Body.Close()
    75  
    76  	reference, err := storeDir(
    77  		ctx,
    78  		encrypt,
    79  		dReader,
    80  		logger,
    81  		putter,
    82  		s.storer.ChunkStore(),
    83  		r.Header.Get(SwarmIndexDocumentHeader),
    84  		r.Header.Get(SwarmErrorDocumentHeader),
    85  		rLevel,
    86  	)
    87  	if err != nil {
    88  		logger.Debug("store dir failed", "error", err)
    89  		logger.Error(nil, "store dir failed")
    90  		switch {
    91  		case errors.Is(err, postage.ErrBucketFull):
    92  			jsonhttp.PaymentRequired(w, "batch is overissued")
    93  		case errors.Is(err, errEmptyDir):
    94  			jsonhttp.BadRequest(w, errEmptyDir)
    95  		case errors.Is(err, tar.ErrHeader):
    96  			jsonhttp.BadRequest(w, "invalid filename in tar archive")
    97  		default:
    98  			jsonhttp.InternalServerError(w, errDirectoryStore)
    99  		}
   100  		ext.LogError(span, err, olog.String("action", "dir.store"))
   101  		return
   102  	}
   103  
   104  	encryptedReference := reference
   105  	if act {
   106  		encryptedReference, err = s.actEncryptionHandler(r.Context(), w, putter, reference, historyAddress)
   107  		if err != nil {
   108  			logger.Debug("access control upload failed", "error", err)
   109  			logger.Error(nil, "access control upload failed")
   110  			switch {
   111  			case errors.Is(err, accesscontrol.ErrNotFound):
   112  				jsonhttp.NotFound(w, "act or history entry not found")
   113  			case errors.Is(err, accesscontrol.ErrInvalidPublicKey) || errors.Is(err, accesscontrol.ErrSecretKeyInfinity):
   114  				jsonhttp.BadRequest(w, "invalid public key")
   115  			case errors.Is(err, accesscontrol.ErrUnexpectedType):
   116  				jsonhttp.BadRequest(w, "failed to create history")
   117  			default:
   118  				jsonhttp.InternalServerError(w, errActUpload)
   119  			}
   120  			return
   121  		}
   122  	}
   123  
   124  	err = putter.Done(reference)
   125  	if err != nil {
   126  		logger.Debug("store dir failed", "error", err)
   127  		logger.Error(nil, "store dir failed")
   128  		jsonhttp.InternalServerError(w, errDirectoryStore)
   129  		ext.LogError(span, err, olog.String("action", "putter.Done"))
   130  		return
   131  	}
   132  
   133  	if tag != 0 {
   134  		w.Header().Set(SwarmTagHeader, fmt.Sprint(tag))
   135  		span.LogFields(olog.Bool("success", true))
   136  	}
   137  	w.Header().Set("Access-Control-Expose-Headers", SwarmTagHeader)
   138  	jsonhttp.Created(w, bzzUploadResponse{
   139  		Reference: encryptedReference,
   140  	})
   141  }
   142  
   143  // storeDir stores all files recursively contained in the directory given as a tar/multipart
   144  // it returns the hash for the uploaded manifest corresponding to the uploaded dir
   145  func storeDir(
   146  	ctx context.Context,
   147  	encrypt bool,
   148  	reader dirReader,
   149  	log log.Logger,
   150  	putter storage.Putter,
   151  	getter storage.Getter,
   152  	indexFilename,
   153  	errorFilename string,
   154  	rLevel redundancy.Level,
   155  ) (swarm.Address, error) {
   156  
   157  	logger := tracing.NewLoggerWithTraceID(ctx, log)
   158  	loggerV1 := logger.V(1).Build()
   159  
   160  	p := requestPipelineFn(putter, encrypt, rLevel)
   161  	ls := loadsave.New(getter, putter, requestPipelineFactory(ctx, putter, encrypt, rLevel))
   162  
   163  	dirManifest, err := manifest.NewDefaultManifest(ls, encrypt)
   164  	if err != nil {
   165  		return swarm.ZeroAddress, err
   166  	}
   167  
   168  	if indexFilename != "" && strings.ContainsRune(indexFilename, '/') {
   169  		return swarm.ZeroAddress, errors.New("index document suffix must not include slash character")
   170  	}
   171  
   172  	filesAdded := 0
   173  
   174  	// iterate through the files in the supplied tar
   175  	for {
   176  		fileInfo, err := reader.Next()
   177  		if errors.Is(err, io.EOF) {
   178  			break
   179  		} else if err != nil {
   180  			return swarm.ZeroAddress, fmt.Errorf("read dir stream: %w", err)
   181  		}
   182  
   183  		fileReference, err := p(ctx, fileInfo.Reader)
   184  		if err != nil {
   185  			return swarm.ZeroAddress, fmt.Errorf("store dir file: %w", err)
   186  		}
   187  		loggerV1.Debug("bzz upload dir: file dir uploaded", "file_path", fileInfo.Path, "address", fileReference)
   188  
   189  		fileMtdt := map[string]string{
   190  			manifest.EntryMetadataContentTypeKey: fileInfo.ContentType,
   191  			manifest.EntryMetadataFilenameKey:    fileInfo.Name,
   192  		}
   193  		// add file entry to dir manifest
   194  		err = dirManifest.Add(ctx, fileInfo.Path, manifest.NewEntry(fileReference, fileMtdt))
   195  		if err != nil {
   196  			return swarm.ZeroAddress, fmt.Errorf("add to manifest: %w", err)
   197  		}
   198  
   199  		filesAdded++
   200  	}
   201  
   202  	// check if files were uploaded through the manifest
   203  	if filesAdded == 0 {
   204  		return swarm.ZeroAddress, errEmptyDir
   205  	}
   206  
   207  	// store website information
   208  	if indexFilename != "" || errorFilename != "" {
   209  		metadata := map[string]string{}
   210  		if indexFilename != "" {
   211  			metadata[manifest.WebsiteIndexDocumentSuffixKey] = indexFilename
   212  		}
   213  		if errorFilename != "" {
   214  			metadata[manifest.WebsiteErrorDocumentPathKey] = errorFilename
   215  		}
   216  		rootManifestEntry := manifest.NewEntry(swarm.ZeroAddress, metadata)
   217  		err = dirManifest.Add(ctx, manifest.RootPath, rootManifestEntry)
   218  		if err != nil {
   219  			return swarm.ZeroAddress, fmt.Errorf("add to manifest: %w", err)
   220  		}
   221  	}
   222  
   223  	// save manifest
   224  	manifestReference, err := dirManifest.Store(ctx)
   225  	if err != nil {
   226  		return swarm.ZeroAddress, fmt.Errorf("store manifest: %w", err)
   227  	}
   228  	loggerV1.Debug("bzz upload dir: uploaded dir finished", "address", manifestReference)
   229  
   230  	return manifestReference, nil
   231  }
   232  
   233  type FileInfo struct {
   234  	Path        string
   235  	Name        string
   236  	ContentType string
   237  	Size        int64
   238  	Reader      io.Reader
   239  }
   240  
   241  type dirReader interface {
   242  	Next() (*FileInfo, error)
   243  }
   244  
   245  type tarReader struct {
   246  	r      *tar.Reader
   247  	logger log.Logger
   248  }
   249  
   250  func (t *tarReader) Next() (*FileInfo, error) {
   251  	for {
   252  		fileHeader, err := t.r.Next()
   253  		if err != nil {
   254  			return nil, err
   255  		}
   256  
   257  		fileName := fileHeader.FileInfo().Name()
   258  		contentType := mime.TypeByExtension(filepath.Ext(fileHeader.Name))
   259  		fileSize := fileHeader.FileInfo().Size()
   260  		filePath := filepath.Clean(fileHeader.Name)
   261  
   262  		if filePath == "." {
   263  			t.logger.Warning("skipping file upload empty path")
   264  			continue
   265  		}
   266  		if runtime.GOOS == "windows" {
   267  			// always use Unix path separator
   268  			filePath = filepath.ToSlash(filePath)
   269  		}
   270  		// only store regular files
   271  		if !fileHeader.FileInfo().Mode().IsRegular() {
   272  			t.logger.Warning("bzz upload dir: skipping file upload as it is not a regular file", "file_path", filePath)
   273  			continue
   274  		}
   275  
   276  		return &FileInfo{
   277  			Path:        filePath,
   278  			Name:        fileName,
   279  			ContentType: contentType,
   280  			Size:        fileSize,
   281  			Reader:      t.r,
   282  		}, nil
   283  	}
   284  }
   285  
   286  // multipart reader returns files added as a multipart form. We will ensure all the
   287  // part headers are passed correctly
   288  type multipartReader struct {
   289  	r *multipart.Reader
   290  }
   291  
   292  func (m *multipartReader) Next() (*FileInfo, error) {
   293  	part, err := m.r.NextPart()
   294  	if err != nil {
   295  		return nil, err
   296  	}
   297  
   298  	filePath := part.FileName()
   299  	if filePath == "" {
   300  		filePath = part.FormName()
   301  	}
   302  
   303  	fileName := filepath.Base(filePath)
   304  
   305  	contentType := part.Header.Get(ContentTypeHeader)
   306  
   307  	contentLength := part.Header.Get(ContentLengthHeader)
   308  
   309  	fileSize, _ := strconv.ParseInt(contentLength, 10, 64)
   310  
   311  	return &FileInfo{
   312  		Path:        filePath,
   313  		Name:        fileName,
   314  		ContentType: contentType,
   315  		Size:        fileSize,
   316  		Reader:      part,
   317  	}, nil
   318  }