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 }