class Rb1drv::OneDriveDir

Attributes

child_count[R]

Public Class Methods

new(od, api_hash) click to toggle source
Calls superclass method
# File lib/rb1drv/onedrive_dir.rb, line 7
def initialize(od, api_hash)
  super
  @child_count = api_hash.dig('folder', 'childCount')
  @cached_gets = {}
end

Public Instance Methods

absolute_path() click to toggle source

@return [String] absolute path of current item

# File lib/rb1drv/onedrive_dir.rb, line 56
def absolute_path
  if @parent_path
    File.join(@parent_path, @name)
  else
    '/'
  end
end
children() click to toggle source

Lists contents of current directory.

@return [Array<OneDriveDir,OneDriveFile>] directories and files whose parent is current directory

# File lib/rb1drv/onedrive_dir.rb, line 16
def children
  return [] if child_count <= 0
  @cached_children ||= @od.request("#{api_path}/children?$top=1000")['value'].map do |child|
    OneDriveItem.smart_new(@od, child)
  end
end
dir?() click to toggle source

Yes

# File lib/rb1drv/onedrive_dir.rb, line 46
def dir?
  true
end
file?() click to toggle source

No

# File lib/rb1drv/onedrive_dir.rb, line 51
def file?
  false
end
get(path) click to toggle source

Get an object by an arbitary path related to current directory.

To get an absolute path, make use of OneDrive#get and not this.

@param path [String] path relative to current directory

@return [OneDriveDir,OneDriveFile,OneDrive404] the drive item you asked

# File lib/rb1drv/onedrive_dir.rb, line 39
def get(path)
  path = "/#{path}" unless path[0] == '/'
  @cached_gets[path] ||=
    OneDriveItem.smart_new(@od, @od.request("#{api_path}:#{path}"))
end
get_child(path) click to toggle source

Get a child object by name inside current directory.

@param path [String] name of a child

@return [OneDriveDir,OneDriveFile,OneDrive404] the drive item you asked

# File lib/rb1drv/onedrive_dir.rb, line 28
def get_child(path)
  children.find { |child| child.name == path } || OneDrive404.new
end
mkdir(name) click to toggle source

Recursively creates empty directories.

@param name [String] directories you'd like to create @return [OneDriveDir] the directory you created

# File lib/rb1drv/onedrive_dir.rb, line 68
def mkdir(name)
  return self if name == '.'
  name = name[1..-1] if name[0] == '/'
  newdir, *remainder = name.split('/')
  subdir = get(newdir)
  unless subdir.dir?
    result = @od.request("#{api_path}/children",
      name: newdir,
      folder: {},
      '@microsoft.graph.conflictBehavior': 'rename'
    )
    subdir = OneDriveDir.new(@od, result)
  end
  remainder.any? ? subdir.mkdir(remainder.join('/')) : subdir
end
upload(filename, overwrite: false, fragment_size: 41_943_040, chunk_size: 1_048_576, target_name: nil) { |:new_segment, file: filename, from: from, to: to| ... } click to toggle source

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

# File lib/rb1drv/onedrive_dir.rb, line 104
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
upload_simple(filename, overwrite:, target_name:) click to toggle source

Uploads a local file into current remote directory using simple upload mode.

@return [OneDriveFile,nil] uploaded file

# File lib/rb1drv/onedrive_dir.rb, line 203
def upload_simple(filename, overwrite:, target_name:)
  target_file = get(target_name)
  exist = target_file.file?
  return if exist && !overwrite
  path = nil
  if exist
    path = "#{target_file.api_path}/content"
  else
    path = "#{api_path}:/#{target_name}:/content"
  end

  query = {
    path: File.join('v1.0/me/', path),
    headers: {
      'Authorization': "Bearer #{@od.access_token.token}",
      'Content-Type': 'application/octet-stream'
    },
    body: File.read(filename)
  }
  result = @od.conn.put(query)
  result = JSON.parse(result.body)
  file = OneDriveFile.new(@od, result)
  file.set_mtime(File.mtime(filename))
end