github.com/cs3org/reva/v2@v2.27.7/pkg/storage/utils/decomposedfs/upload/session.go (about) 1 // Copyright 2018-2023 CERN 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 // 15 // In applying this license, CERN does not waive the privileges and immunities 16 // granted to it by virtue of its status as an Intergovernmental Organization 17 // or submit itself to any jurisdiction. 18 19 package upload 20 21 import ( 22 "context" 23 "encoding/json" 24 "os" 25 "path/filepath" 26 "strconv" 27 "time" 28 29 "github.com/google/renameio/v2" 30 tusd "github.com/tus/tusd/v2/pkg/handler" 31 32 userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1" 33 provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1" 34 typespb "github.com/cs3org/go-cs3apis/cs3/types/v1beta1" 35 "github.com/cs3org/reva/v2/pkg/appctx" 36 ctxpkg "github.com/cs3org/reva/v2/pkg/ctx" 37 "github.com/cs3org/reva/v2/pkg/storage/utils/decomposedfs/node" 38 "github.com/cs3org/reva/v2/pkg/utils" 39 ) 40 41 // OcisSession extends tus upload lifecycle with postprocessing steps. 42 type OcisSession struct { 43 store OcisStore 44 // for now, we keep the json files in the uploads folder 45 info tusd.FileInfo 46 } 47 48 // Context returns a context with the user, logger and lockid used when initiating the upload session 49 func (s *OcisSession) Context(ctx context.Context) context.Context { // restore logger from file info 50 sub := s.store.log.With().Int("pid", os.Getpid()).Logger() 51 ctx = appctx.WithLogger(ctx, &sub) 52 ctx = ctxpkg.ContextSetLockID(ctx, s.lockID()) 53 ctx = ctxpkg.ContextSetUser(ctx, s.executantUser()) 54 return ctxpkg.ContextSetInitiator(ctx, s.InitiatorID()) 55 } 56 57 func (s *OcisSession) lockID() string { 58 return s.info.MetaData["lockid"] 59 } 60 func (s *OcisSession) executantUser() *userpb.User { 61 var o *typespb.Opaque 62 _ = json.Unmarshal([]byte(s.info.Storage["UserOpaque"]), &o) 63 return &userpb.User{ 64 Id: &userpb.UserId{ 65 Type: userpb.UserType(userpb.UserType_value[s.info.Storage["UserType"]]), 66 Idp: s.info.Storage["Idp"], 67 OpaqueId: s.info.Storage["UserId"], 68 }, 69 Username: s.info.Storage["UserName"], 70 DisplayName: s.info.Storage["UserDisplayName"], 71 Opaque: o, 72 } 73 } 74 75 // Purge deletes the upload session metadata and written binary data 76 func (s *OcisSession) Purge(ctx context.Context) error { 77 _, span := tracer.Start(ctx, "Purge") 78 defer span.End() 79 sessionPath := sessionPath(s.store.root, s.info.ID) 80 if err := os.Remove(sessionPath); err != nil { 81 return err 82 } 83 if err := os.Remove(s.binPath()); err != nil { 84 return err 85 } 86 return nil 87 } 88 89 // TouchBin creates a file to contain the binary data. It's size will be used to keep track of the tus upload offset. 90 func (s *OcisSession) TouchBin() error { 91 file, err := os.OpenFile(s.binPath(), os.O_CREATE|os.O_WRONLY, defaultFilePerm) 92 if err != nil { 93 return err 94 } 95 return file.Close() 96 } 97 98 // Persist writes the upload session metadata to disk 99 // events can update the scan outcome and the finished event might read an empty file because of race conditions 100 // so we need to lock the file while writing and use atomic writes 101 func (s *OcisSession) Persist(ctx context.Context) error { 102 _, span := tracer.Start(ctx, "Persist") 103 defer span.End() 104 sessionPath := sessionPath(s.store.root, s.info.ID) 105 // create folder structure (if needed) 106 if err := os.MkdirAll(filepath.Dir(sessionPath), 0700); err != nil { 107 return err 108 } 109 110 var d []byte 111 d, err := json.Marshal(s.info) 112 if err != nil { 113 return err 114 } 115 return renameio.WriteFile(sessionPath, d, 0600) 116 } 117 118 // ToFileInfo returns tus compatible FileInfo so the tus handler can access the upload offset 119 func (s *OcisSession) ToFileInfo() tusd.FileInfo { 120 return s.info 121 } 122 123 // ProviderID returns the provider id 124 func (s *OcisSession) ProviderID() string { 125 return s.info.MetaData["providerID"] 126 } 127 128 // SpaceID returns the space id 129 func (s *OcisSession) SpaceID() string { 130 return s.info.Storage["SpaceRoot"] 131 } 132 133 // NodeID returns the node id 134 func (s *OcisSession) NodeID() string { 135 return s.info.Storage["NodeId"] 136 } 137 138 // NodeParentID returns the nodes parent id 139 func (s *OcisSession) NodeParentID() string { 140 return s.info.Storage["NodeParentId"] 141 } 142 143 // NodeExists returns wether or not the node existed during InitiateUpload. 144 // FIXME If two requests try to write the same file they both will store a new 145 // random node id in the session and try to initialize a new node when 146 // finishing the upload. The second request will fail with an already exists 147 // error when trying to create the symlink for the node in the parent directory. 148 // A node should be created as part of InitiateUpload. When listing a directory 149 // we can decide if we want to skip the entry, or expose uploed progress 150 // information. But that is a bigger change and might involve client work. 151 func (s *OcisSession) NodeExists() bool { 152 return s.info.Storage["NodeExists"] == "true" 153 } 154 155 // HeaderIfMatch returns the if-match header for the upload session 156 func (s *OcisSession) HeaderIfMatch() string { 157 return s.info.MetaData["if-match"] 158 } 159 160 // HeaderIfNoneMatch returns the if-none-match header for the upload session 161 func (s *OcisSession) HeaderIfNoneMatch() string { 162 return s.info.MetaData["if-none-match"] 163 } 164 165 // HeaderIfUnmodifiedSince returns the if-unmodified-since header for the upload session 166 func (s *OcisSession) HeaderIfUnmodifiedSince() string { 167 return s.info.MetaData["if-unmodified-since"] 168 } 169 170 // Node returns the node for the session 171 func (s *OcisSession) Node(ctx context.Context) (*node.Node, error) { 172 return node.ReadNode(ctx, s.store.lu, s.SpaceID(), s.info.Storage["NodeId"], false, nil, true) 173 } 174 175 // ID returns the upload session id 176 func (s *OcisSession) ID() string { 177 return s.info.ID 178 } 179 180 // Filename returns the name of the node which is not the same as the name af the file being uploaded for legacy chunked uploads 181 func (s *OcisSession) Filename() string { 182 return s.info.Storage["NodeName"] 183 } 184 185 // Chunk returns the chunk name when a legacy chunked upload was started 186 func (s *OcisSession) Chunk() string { 187 return s.info.Storage["Chunk"] 188 } 189 190 // SetMetadata is used to fill the upload metadata that will be exposed to the end user 191 func (s *OcisSession) SetMetadata(key, value string) { 192 s.info.MetaData[key] = value 193 } 194 195 // SetStorageValue is used to set metadata only relevant for the upload session implementation 196 func (s *OcisSession) SetStorageValue(key, value string) { 197 s.info.Storage[key] = value 198 } 199 200 // SetSize will set the upload size of the underlying tus info. 201 func (s *OcisSession) SetSize(size int64) { 202 s.info.Size = size 203 } 204 205 // SetSizeIsDeferred is uset to change the SizeIsDeferred property of the underlying tus info. 206 func (s *OcisSession) SetSizeIsDeferred(value bool) { 207 s.info.SizeIsDeferred = value 208 } 209 210 // Dir returns the directory to which the upload is made 211 // TODO get rid of Dir(), whoever consumes the reference should be able to deal 212 // with a relative reference. 213 // Dir is only used to: 214 // - fill the Path property when emitting the UploadReady event after 215 // postprocessing finished. I wonder why the UploadReady contains a finished 216 // flag ... maybe multiple distinct events would make more sense. 217 // - build the reference that is passed to the FileUploaded event in the 218 // UploadFinishedFunc callback passed to the Upload call used for simple 219 // datatx put requests 220 // 221 // AFAICT only search and audit services consume the path. 222 // - search needs to index from the root anyway. And it only needs the most 223 // recent path to put it in the index. So it should already be able to deal 224 // with an id based reference. 225 // - audit on the other hand needs to log events with the path at the state of 226 // the event ... so it does need the full path. 227 // 228 // I think we can safely determine the path later, right before emitting the 229 // event. And maybe make it configurable, because only audit needs it, anyway. 230 func (s *OcisSession) Dir() string { 231 return s.info.Storage["Dir"] 232 } 233 234 // Size returns the upload size 235 func (s *OcisSession) Size() int64 { 236 return s.info.Size 237 } 238 239 // SizeDiff returns the size diff that was calculated after postprocessing 240 func (s *OcisSession) SizeDiff() int64 { 241 sizeDiff, _ := strconv.ParseInt(s.info.MetaData["sizeDiff"], 10, 64) 242 return sizeDiff 243 } 244 245 // Reference returns a reference that can be used to access the uploaded resource 246 func (s *OcisSession) Reference() provider.Reference { 247 return provider.Reference{ 248 ResourceId: &provider.ResourceId{ 249 StorageId: s.info.MetaData["providerID"], 250 SpaceId: s.info.Storage["SpaceRoot"], 251 OpaqueId: s.info.Storage["NodeId"], 252 }, 253 // Path is not used 254 } 255 } 256 257 // Executant returns the id of the user that initiated the upload session 258 func (s *OcisSession) Executant() userpb.UserId { 259 return userpb.UserId{ 260 Type: userpb.UserType(userpb.UserType_value[s.info.Storage["UserType"]]), 261 Idp: s.info.Storage["Idp"], 262 OpaqueId: s.info.Storage["UserId"], 263 } 264 } 265 266 // SetExecutant is used to remember the user that initiated the upload session 267 func (s *OcisSession) SetExecutant(u *userpb.User) { 268 s.info.Storage["Idp"] = u.GetId().GetIdp() 269 s.info.Storage["UserId"] = u.GetId().GetOpaqueId() 270 s.info.Storage["UserType"] = utils.UserTypeToString(u.GetId().Type) 271 s.info.Storage["UserName"] = u.GetUsername() 272 s.info.Storage["UserDisplayName"] = u.GetDisplayName() 273 274 b, _ := json.Marshal(u.GetOpaque()) 275 s.info.Storage["UserOpaque"] = string(b) 276 } 277 278 // Offset returns the current upload offset 279 func (s *OcisSession) Offset() int64 { 280 return s.info.Offset 281 } 282 283 // SpaceOwner returns the id of the space owner 284 func (s *OcisSession) SpaceOwner() *userpb.UserId { 285 return &userpb.UserId{ 286 // idp and type do not seem to be consumed and the node currently only stores the user id anyway 287 OpaqueId: s.info.Storage["SpaceOwnerOrManager"], 288 } 289 } 290 291 // Expires returns the time the upload session expires 292 func (s *OcisSession) Expires() time.Time { 293 var t time.Time 294 if value, ok := s.info.MetaData["expires"]; ok { 295 t, _ = utils.MTimeToTime(value) 296 } 297 return t 298 } 299 300 // MTime returns the mtime to use for the uploaded file 301 func (s *OcisSession) MTime() time.Time { 302 var t time.Time 303 if value, ok := s.info.MetaData["mtime"]; ok { 304 t, _ = utils.MTimeToTime(value) 305 } 306 return t 307 } 308 309 // IsProcessing returns true if all bytes have been received. The session then has entered postprocessing state. 310 func (s *OcisSession) IsProcessing() bool { 311 // We might need a more sophisticated way to determine processing status soon 312 return s.info.Size == s.info.Offset && s.info.MetaData["scanResult"] == "" 313 } 314 315 // binPath returns the path to the file storing the binary data. 316 func (s *OcisSession) binPath() string { 317 return filepath.Join(s.store.root, "uploads", s.info.ID) 318 } 319 320 // InitiatorID returns the id of the initiating client 321 func (s *OcisSession) InitiatorID() string { 322 return s.info.MetaData["initiatorid"] 323 } 324 325 // SetScanData sets virus scan data to the upload session 326 func (s *OcisSession) SetScanData(result string, date time.Time) { 327 s.info.MetaData["scanResult"] = result 328 s.info.MetaData["scanDate"] = date.Format(time.RFC3339) 329 } 330 331 // ScanData returns the virus scan data 332 func (s *OcisSession) ScanData() (string, time.Time) { 333 date := s.info.MetaData["scanDate"] 334 if date == "" { 335 return "", time.Time{} 336 } 337 d, _ := time.Parse(time.RFC3339, date) 338 return s.info.MetaData["scanResult"], d 339 } 340 341 // sessionPath returns the path to the .info file storing the file's info. 342 func sessionPath(root, id string) string { 343 return filepath.Join(root, "uploads", id+".info") 344 }