github.com/jstaf/onedriver@v0.14.2-0.20240420231225-f07678f9e6ef/fs/upload_manager.go (about) 1 package fs 2 3 import ( 4 "encoding/json" 5 "time" 6 7 "github.com/jstaf/onedriver/fs/graph" 8 "github.com/rs/zerolog/log" 9 bolt "go.etcd.io/bbolt" 10 ) 11 12 const maxUploadsInFlight = 5 13 14 var bucketUploads = []byte("uploads") 15 16 // UploadManager is used to manage and retry uploads. 17 type UploadManager struct { 18 queue chan *UploadSession 19 deletionQueue chan string 20 sessions map[string]*UploadSession 21 inFlight uint8 // number of sessions in flight 22 auth *graph.Auth 23 fs *Filesystem 24 db *bolt.DB 25 } 26 27 // NewUploadManager creates a new queue/thread for uploads 28 func NewUploadManager(duration time.Duration, db *bolt.DB, fs *Filesystem, auth *graph.Auth) *UploadManager { 29 manager := UploadManager{ 30 queue: make(chan *UploadSession), 31 deletionQueue: make(chan string, 1000), // FIXME - why does this chan need to be buffered now??? 32 sessions: make(map[string]*UploadSession), 33 auth: auth, 34 db: db, 35 fs: fs, 36 } 37 db.View(func(tx *bolt.Tx) error { 38 // Add any incomplete sessions from disk - any sessions here were never 39 // finished. The most likely cause of this is that the user shut off 40 // their computer or closed the program after starting the upload. 41 b := tx.Bucket(bucketUploads) 42 if b == nil { 43 // bucket does not exist yet, bail out early 44 return nil 45 } 46 return b.ForEach(func(key []byte, val []byte) error { 47 session := &UploadSession{} 48 err := json.Unmarshal(val, session) 49 if err != nil { 50 log.Error().Err(err).Msg("Failure restoring upload sessions from disk.") 51 return err 52 } 53 if session.getState() != uploadNotStarted { 54 manager.inFlight++ 55 } 56 session.cancel(auth) // uploads are currently non-resumable 57 manager.sessions[session.ID] = session 58 return nil 59 }) 60 }) 61 go manager.uploadLoop(duration) 62 return &manager 63 } 64 65 // uploadLoop manages the deduplication and tracking of uploads 66 func (u *UploadManager) uploadLoop(duration time.Duration) { 67 ticker := time.NewTicker(duration) 68 for { 69 select { 70 case session := <-u.queue: // new sessions 71 // deduplicate sessions for the same item 72 if old, exists := u.sessions[session.ID]; exists { 73 old.cancel(u.auth) 74 } 75 contents, _ := json.Marshal(session) 76 u.db.Batch(func(tx *bolt.Tx) error { 77 // persist to disk in case the user shuts off their computer or 78 // kills onedriver prematurely 79 b, _ := tx.CreateBucketIfNotExists(bucketUploads) 80 return b.Put([]byte(session.ID), contents) 81 }) 82 u.sessions[session.ID] = session 83 84 case cancelID := <-u.deletionQueue: // remove uploads for deleted items 85 u.finishUpload(cancelID) 86 87 case <-ticker.C: // periodically start uploads, or remove them if done/failed 88 for _, session := range u.sessions { 89 switch session.getState() { 90 case uploadNotStarted: 91 // max active upload sessions are capped at this limit for faster 92 // uploads of individual files and also to prevent possible server- 93 // side throttling that can cause errors. 94 if u.inFlight < maxUploadsInFlight { 95 u.inFlight++ 96 go session.Upload(u.auth) 97 } 98 99 case uploadErrored: 100 session.retries++ 101 if session.retries > 5 { 102 log.Error(). 103 Str("id", session.ID). 104 Str("name", session.Name). 105 Err(session). 106 Int("retries", session.retries). 107 Msg("Upload session failed too many times, cancelling session.") 108 u.finishUpload(session.ID) 109 } 110 111 log.Warn(). 112 Str("id", session.ID). 113 Str("name", session.Name). 114 Err(session). 115 Msg("Upload session failed, will retry from beginning.") 116 session.cancel(u.auth) // cancel large sessions 117 session.setState(uploadNotStarted, nil) 118 119 case uploadComplete: 120 log.Info(). 121 Str("id", session.ID). 122 Str("oldID", session.OldID). 123 Str("name", session.Name). 124 Msg("Upload completed!") 125 126 // ID changed during upload, move to new ID 127 if session.OldID != session.ID { 128 err := u.fs.MoveID(session.OldID, session.ID) 129 if err != nil { 130 log.Error(). 131 Str("id", session.ID). 132 Str("oldID", session.OldID). 133 Str("name", session.Name). 134 Err(err). 135 Msg("Could not move inode to new ID!") 136 } 137 } 138 139 // inode will exist at the new ID now, but we check if inode 140 // is nil to see if the item has been deleted since upload start 141 if inode := u.fs.GetID(session.ID); inode != nil { 142 inode.Lock() 143 inode.DriveItem.ETag = session.ETag 144 inode.Unlock() 145 } 146 147 // the old ID is the one that was used to add it to the queue. 148 // cleanup the session. 149 u.finishUpload(session.OldID) 150 } 151 } 152 } 153 } 154 } 155 156 // QueueUpload queues an item for upload. 157 func (u *UploadManager) QueueUpload(inode *Inode) error { 158 data := u.fs.getInodeContent(inode) 159 session, err := NewUploadSession(inode, data) 160 if err == nil { 161 u.queue <- session 162 } 163 return err 164 } 165 166 // CancelUpload is used to kill any pending uploads for a session 167 func (u *UploadManager) CancelUpload(id string) { 168 u.deletionQueue <- id 169 } 170 171 // finishUpload is an internal method that gets called when a session is 172 // completed. It cancels the session if one was in progress, and then deletes 173 // it from both memory and disk. 174 func (u *UploadManager) finishUpload(id string) { 175 if session, exists := u.sessions[id]; exists { 176 session.cancel(u.auth) 177 } 178 u.db.Batch(func(tx *bolt.Tx) error { 179 if b := tx.Bucket(bucketUploads); b != nil { 180 b.Delete([]byte(id)) 181 } 182 return nil 183 }) 184 if u.inFlight > 0 { 185 u.inFlight-- 186 } 187 delete(u.sessions, id) 188 }