def upload(filename, overwrite: false, fragment_size: 41_943_040, chunk_size: 1_048_576, target_name: nil, &block)
      raise ArgumentError.new('File not found') unless File.exist?(filename)
      conn = nil
      file_size = File.size(filename)
      target_name ||= File.basename(filename)
      return upload_simple(filename, overwrite: overwrite, target_name: target_name) if file_size <= 4_096_000

      resume_file = "#{filename}.1drv_upload"
      resume_session = JSON.parse(File.read(resume_file)) rescue nil if File.exist?(resume_file)
      old_file = OneDriveItem.smart_new(@od, @od.request("#{api_path}:/#{target_name}"))
      new_file = nil

      result = nil
      loop do
        catch :restart do
          if resume_session && resume_session['session_url']
            conn = Excon.new(resume_session['session_url'], idempotent: true)
            loop do
              result = JSON.parse(conn.get.body)
              break unless result.dig('error', 'code') == 'accessDenied'
              sleep 5
            end
            resume_position = result.dig('nextExpectedRanges', 0)&.split('-')&.first&.to_i or resume_session = nil
          end

          resume_position ||= 0

          if resume_session
            file_size == resume_session['source_size'] or resume_session = nil
          end

          until resume_session && resume_session['session_url'] do
            result = @od.request("#{api_path}:/#{target_name}:/createUploadSession", item: {'@microsoft.graph.conflictBehavior': overwrite ? 'replace' : 'rename'})
            if result['uploadUrl']
              resume_session = {
                'session_url' => result['uploadUrl'],
                'source_size' => File.size(filename),
                'fragment_size' => fragment_size
              }
              File.write(resume_file, JSON.pretty_generate(resume_session))
              conn = Excon.new(resume_session['session_url'], idempotent: true)
              break
            end
            sleep 15
          end

          new_file = nil
          File.open(filename, mode: 'rb', external_encoding: Encoding::BINARY) do |f|
            resume_position.step(file_size - 1, resume_session['fragment_size']) do |from|
              to = [from + resume_session['fragment_size'], file_size].min - 1
              len = to - from + 1
              headers = {
                'Content-Length': len.to_s,
                'Content-Range': "bytes #{from}-#{to}/#{file_size}"
              }
              @od.logger.info "Uploading #{from}-#{to}/#{file_size}" if @od.logger
              yield :new_segment, file: filename, from: from, to: to if block_given?
              sliced_io = SlicedIO.new(f, from, to) do |progress, total|
                yield :progress, file: filename, from: from, to: to, progress: progress, total: total if block_given?
              end
              begin
                result = conn.put headers: headers, chunk_size: chunk_size, body: sliced_io, retry_limit: 2
                raise IOError if result.body.include? 'accessDenied'
              rescue Excon::Error::Socket, IOError
                # Probably server rejected this request
                throw :restart
              rescue Excon::Error::Timeout
                conn = Excon.new(resume_session['session_url'], idempotent: true)
                yield :retry, file: filename, from: from, to: to if block_given?
                retry
              ensure
                yield :finish_segment, file: filename, from: from, to: to if block_given?
              end
              throw :restart if result.body.include?('</html>')
              result = JSON.parse(result.body)
              new_file = OneDriveFile.new(@od, result) if result.dig('file')
            end
          end
          throw :restart unless new_file&.file?
          break
        end
        # catch :restart here
        6.times do
          new_file = OneDriveItem.smart_new(@od, @od.request("#{api_path}:/#{target_name}"))
          break if new_file.file? && new_file.id != old_file.id
          sleep 10 # wait for server to process the previous request
        end
        break if new_file.file? && new_file.id != old_file.id
        # and retry the whole process
      end

      # upload completed
      File.unlink(resume_file)
      return new_file.set_mtime(File.mtime(filename))
    end
def upload(filename, overwrite: false, fragment_size: 41_943_040, chunk_size: 1_048_576, target_name: nil, &block)
      raise ArgumentError.new('File not found') unless File.exist?(filename)
      conn = nil
      file_size = File.size(filename)
      target_name ||= File.basename(filename)
      return upload_simple(filename, overwrite: overwrite, target_name: target_name) if file_size <= 4_096_000

      resume_file = "#{filename}.1drv_upload"
      resume_session = JSON.parse(File.read(resume_file)) rescue nil if File.exist?(resume_file)
      old_file = OneDriveItem.smart_new(@od, @od.request("#{api_path}:/#{target_name}"))
      new_file = nil

      result = nil
      loop do
        catch :restart do
          if resume_session && resume_session['session_url']
            conn = Excon.new(resume_session['session_url'], idempotent: true)
            loop do
              result = JSON.parse(conn.get.body)
              break unless result.dig('error', 'code') == 'accessDenied'
              sleep 5
            end
            resume_position = result.dig('nextExpectedRanges', 0)&.split('-')&.first&.to_i or resume_session = nil
          end

          resume_position ||= 0

          if resume_session
            file_size == resume_session['source_size'] or resume_session = nil
          end

          until resume_session && resume_session['session_url'] do
            result = @od.request("#{api_path}:/#{target_name}:/createUploadSession", item: {'@microsoft.graph.conflictBehavior': overwrite ? 'replace' : 'rename'})
            if result['uploadUrl']
              resume_session = {
                'session_url' => result['uploadUrl'],
                'source_size' => File.size(filename),
                'fragment_size' => fragment_size
              }
              File.write(resume_file, JSON.pretty_generate(resume_session))
              conn = Excon.new(resume_session['session_url'], idempotent: true)
              break
            end
            sleep 15
          end

          new_file = nil
          File.open(filename, mode: 'rb', external_encoding: Encoding::BINARY) do |f|
            resume_position.step(file_size - 1, resume_session['fragment_size']) do |from|
              to = [from + resume_session['fragment_size'], file_size].min - 1
              len = to - from + 1
              headers = {
                'Content-Length': len.to_s,
                'Content-Range': "bytes #{from}-#{to}/#{file_size}"
              }
              @od.logger.info "Uploading #{from}-#{to}/#{file_size}" if @od.logger
              yield :new_segment, file: filename, from: from, to: to if block_given?
              sliced_io = SlicedIO.new(f, from, to) do |progress, total|
                yield :progress, file: filename, from: from, to: to, progress: progress, total: total if block_given?
              end
              begin
                result = conn.put headers: headers, chunk_size: chunk_size, body: sliced_io, retry_limit: 2
                raise IOError if result.body.include? 'accessDenied'
              rescue Excon::Error::Socket, IOError
                # Probably server rejected this request
                throw :restart
              rescue Excon::Error::Timeout
                conn = Excon.new(resume_session['session_url'], idempotent: true)
                yield :retry, file: filename, from: from, to: to if block_given?
                retry
              ensure
                yield :finish_segment, file: filename, from: from, to: to if block_given?
              end
              throw :restart if result.body.include?('</html>')
              result = JSON.parse(result.body)
              new_file = OneDriveFile.new(@od, result) if result.dig('file')
            end
          end
          throw :restart unless new_file&.file?
          break
        end
        # catch :restart here
        6.times do
          new_file = OneDriveItem.smart_new(@od, @od.request("#{api_path}:/#{target_name}"))
          break if new_file.file? && new_file.id != old_file.id
          sleep 10 # wait for server to process the previous request
        end
        break if new_file.file? && new_file.id != old_file.id
        # and retry the whole process
      end

      # upload completed
      File.unlink(resume_file)
      return new_file.set_mtime(File.mtime(filename))
    end
Uploads a local file into current remote directory.
 For files no larger than 4000KiB, uses simple upload mode.
 For larger files, uses large file upload mode.

 Unfinished download is stored as +target_name.incomplete+ and renamed upon completion.

 @param filename [String] local filename you'd like to upload
 @param overwrite [Boolean] whether to overwrite remote file, or not
   If false:
   For larger files, it renames the uploaded file
   For small files, it skips the file
   Always check existence beforehand if you need consistant behavior
 @param fragment_size [Integer] fragment size for each upload session, recommended to be multiple of 320KiB
 @param chunk_size [Integer] IO size for each disk read request and progress notification
 @param target_name [String] desired remote filename, a relative path to current directory
 @return [OneDriveFile,nil] uploaded file

 @yield [event, status] for receive progress notification
 @yieldparam event [Symbol] event of this notification
 @yieldparam status [{Symbol => String,Integer}] details