Class: Rb1drv::OneDriveDir

Inherits:
OneDriveItem show all
Defined in:
lib/rb1drv/onedrive_dir.rb

Instance Attribute Summary collapse

Attributes inherited from OneDriveItem

#ctime, #cuser, #eTag, #id, #mtime, #muser, #name, #parent_path, #remote_drive_id, #remote_id, #size

Instance Method Summary collapse

Constructor Details

#initialize(od, api_hash) ⇒ OneDriveDir

Returns a new instance of OneDriveDir.



7
8
9
10
11
# File 'lib/rb1drv/onedrive_dir.rb', line 7

def initialize(od, api_hash)
  super
  @child_count = api_hash.dig('folder', 'childCount')
  @cached_gets = {}
end

Instance Attribute Details

#child_countObject (readonly)

Returns the value of attribute child_count.



6
7
8
# File 'lib/rb1drv/onedrive_dir.rb', line 6

def child_count
  @child_count
end

Instance Method Details

#absolute_pathString

Returns absolute path of current item.

Returns:

  • (String)

    absolute path of current item



56
57
58
59
60
61
62
# File 'lib/rb1drv/onedrive_dir.rb', line 56

def absolute_path
  if @parent_path
    File.join(@parent_path, @name)
  else
    '/'
  end
end

#childrenArray<OneDriveDir,OneDriveFile>

Lists contents of current directory.

Returns:



16
17
18
19
20
21
# File 'lib/rb1drv/onedrive_dir.rb', line 16

def children
  return [] if child_count <= 0
  @cached_children ||= @od.request("#{api_path}/children")['value'].map do |child|
    OneDriveItem.smart_new(@od, child)
  end
end

#dir?Boolean

Yes

Returns:

  • (Boolean)


46
47
48
# File 'lib/rb1drv/onedrive_dir.rb', line 46

def dir?
  true
end

#file?Boolean

No

Returns:

  • (Boolean)


51
52
53
# File 'lib/rb1drv/onedrive_dir.rb', line 51

def file?
  false
end

#get(path) ⇒ OneDriveDir, ...

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

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

Parameters:

  • path (String)

    path relative to current directory

Returns:



39
40
41
42
43
# 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) ⇒ OneDriveDir, ...

Get a child object by name inside current directory.

Parameters:

  • path (String)

    name of a child

Returns:



28
29
30
# File 'lib/rb1drv/onedrive_dir.rb', line 28

def get_child(path)
  children.find { |child| child.name == path } || OneDrive404.new
end

#mkdir(name) ⇒ OneDriveDir

Recursively creates empty directories.

Parameters:

  • name (String)

    directories you’d like to create

Returns:



68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
# 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

#set_mtime(file, time) ⇒ Object



215
216
217
# File 'lib/rb1drv/onedrive_dir.rb', line 215

def set_mtime(file, time)
  OneDriveFile.new(@od, @od.request(file.api_path, {fileSystemInfo: {lastModifiedDateTime: time.utc.iso8601}}, :patch))
end

#upload(filename, overwrite: false, fragment_size: 41_943_040, chunk_size: 1_048_576, target_name: nil) {|event, status| ... } ⇒ OneDriveFile?

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.

Parameters:

  • filename (String)

    local filename you’d like to upload

  • overwrite (Boolean) (defaults to: false)

    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

  • fragment_size (Integer) (defaults to: 41_943_040)

    fragment size for each upload session, recommended to be multiple of 320KiB

  • chunk_size (Integer) (defaults to: 1_048_576)

    IO size for each disk read request and progress notification

  • target_name (String) (defaults to: nil)

    desired remote filename, a relative path to current directory

Yields:

  • (event, status)

    for receive progress notification

Yield Parameters:

  • event (Symbol)

    event of this notification

  • status ({Symbol => String,Integer})

    details

Returns:

Raises:

  • (ArgumentError)


104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
# 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)

  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'})
        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)
        sleep 15 unless result['uploadUrl']
      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, read_timeout: 15, write_timeout: 15, retry_limit: 2
            raise IOError if result.body.include? 'accessDenied'
          rescue Excon::Error::Timeout, Excon::Error::Socket
            conn = Excon.new(resume_session['session_url'], idempotent: true)
            yield :retry, file: filename, from: from, to: to if block_given?
            retry
          rescue IOError
            conn = Excon.new(resume_session['session_url'], idempotent: true)
            yield :retry, file: filename, from: from, to: to if block_given?
            sleep 60
            retry
          end
          yield :finish_segment, file: filename, from: from, to: to if block_given?
          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?
      File.unlink(resume_file)
      return set_mtime(new_file, File.mtime(filename))
    end
    # catch :restart here
    sleep 60 # and retry the whole process
  end
end

#upload_simple(filename, overwrite:, target_name:) ⇒ OneDriveFile?

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

Returns:



190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
# File 'lib/rb1drv/onedrive_dir.rb', line 190

def upload_simple(filename, overwrite:, target_name:)
  target_file = get(filename)
  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)
  set_mtime(file, File.mtime(filename))
end