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  }