Module: Rfd::FileOps

Included in:
Controller
Defined in:
lib/rfd/file_ops.rb

Constant Summary collapse

SORT_KEYS =
{
  's' => :size, 'S' => :size,
  't' => :mtime, 'c' => :ctime, 'u' => :atime, 'e' => :extname
}.freeze

Instance Method Summary collapse

Instance Method Details

#cd(dir = '~', pushd: true) ⇒ Object

Change the current directory.



6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# File 'lib/rfd/file_ops.rb', line 6

def cd(dir = '~', pushd: true)
  dir = load_item path: expand_path(dir) unless dir.is_a? Item
  unless dir.zip?
    Dir.chdir dir
    @current_zip = nil
  else
    @current_zip = dir
  end
  if current_dir && pushd
    @dir_history << current_dir
    @dir_history.shift if @dir_history.size > 100
  end
  @current_dir, @current_page, @current_row = dir, 0, nil
  main.activate_pane 0
  ls
  @current_dir
rescue Errno::EACCES, Errno::ENOENT => e
  command_line.show_error e.message
  nil
end

#chmod(mode = nil) ⇒ Object

Change the file permission of the selected files and directories.

Parameters

  • mode - Unix chmod string (e.g. +w, g-r, 755, 0644)



75
76
77
78
79
80
81
82
83
84
# File 'lib/rfd/file_ops.rb', line 75

def chmod(mode = nil)
  return unless mode
  begin
    Integer mode
    mode = Integer mode.size == 3 ? "0#{mode}" : mode
  rescue ArgumentError
  end
  FileUtils.chmod mode, selected_items.map(&:path)
  ls
end

#chown(user_and_group) ⇒ Object

Change the file owner of the selected files and directories.

Parameters

  • user_and_group - user name and group name separated by : (e.g. alice, nobody:nobody, :admin)



90
91
92
93
94
95
# File 'lib/rfd/file_ops.rb', line 90

def chown(user_and_group)
  return unless user_and_group
  user, group = user_and_group.split(':').map {|s| s == '' ? nil : s}
  FileUtils.chown user, group, selected_items.map(&:path)
  ls
end

#clipboardObject

Copy selected files and directories’ path into clipboard.



340
341
342
343
# File 'lib/rfd/file_ops.rb', line 340

def clipboard
  cmd = clipboard_command
  IO.popen(cmd, 'w') {|f| f << selected_items.map(&:path).join(' ')} if cmd
end

#cp(dest) ⇒ Object

Copy selected files and directories to the destination.



168
169
170
171
172
173
174
175
176
177
178
179
180
181
# File 'lib/rfd/file_ops.rb', line 168

def cp(dest)
  unless in_zip?
    src = (m = marked_items).any? ? m.map(&:path) : current_item
    FileUtils.cp_r src, expand_path(dest)
  else
    raise 'cping multiple items in .zip is not supported.' if selected_items.size > 1
    Zip::File.open(current_zip) do |zip|
      entry = zip.find_entry(selected_items.first.name).dup
      entry.name, entry.name_length = dest, dest.size
      zip.instance_variable_get(:@entry_set) << entry
    end
  end
  ls
end

#deleteObject

Delete selected files and directories.



250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
# File 'lib/rfd/file_ops.rb', line 250

def delete
  unless in_zip?
    FileUtils.rm_rf selected_items.map(&:path)
  else
    Zip::File.open(current_zip) do |zip|
      zip.select {|e| selected_items.map(&:name).include? e.to_s}.each do |entry|
        if entry.name_is_directory?
          zip.dir.delete entry.to_s
        else
          zip.file.delete entry.to_s
        end
      end
    end
  end
  @current_row -= selected_items.count {|i| i.index <= current_row}
  ls
end

#fetch_items_from_filesystem_or_zipObject

Fetch files from current directory or current .zip file.



98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
# File 'lib/rfd/file_ops.rb', line 98

def fetch_items_from_filesystem_or_zip
  unless in_zip?
    dot = load_item(dir: current_dir, name: '.')
    dotdot = load_item(dir: current_dir, name: '..')
    @items = [dot, dotdot, *Dir.children(current_dir).map { |fn| load_item dir: current_dir, name: fn }]
  else
    @items = [load_item(dir: current_dir, name: '.', stat: File.stat(current_dir)),
      load_item(dir: current_dir, name: '..', stat: File.stat(File.dirname(current_dir)))]
    Zip::File.open(current_dir) do |zf|
      zf.each do |entry|
        next if entry.name_is_directory?
        stat = zf.file.stat entry.name
        @items << load_item(dir: current_dir, name: entry.name, stat: stat)
      end
    end
  end
rescue Errno::EACCES => e
  command_line.show_error e.message
  @items ||= []
rescue Zip::Error => e
  command_line.show_error "ZIP error: #{e.message}"
end

#grep(pattern = '.*') ⇒ Object

Search files and directories from the current directory, and update the screen.

  • pattern - Search pattern against file names in Ruby Regexp string.

Example

a : Search files that contains the letter “a” in their file name .*.pdf$ : Search PDF files



153
154
155
156
157
158
159
160
161
162
163
164
165
# File 'lib/rfd/file_ops.rb', line 153

def grep(pattern = '.*')
  regexp = Regexp.new(pattern)
  fetch_items_from_filesystem_or_zip
  @items = items.shift(2) + items.select {|i| i.name =~ regexp}
  sort_items_according_to_current_direction
  draw_items
  draw_total_items
  move_cursor
rescue RegexpError => e
  command_line.show_error "Invalid regex: #{e.message}"
  switch_page 0
  move_cursor
end

#lsObject

Fetch files from current directory. Then update each windows reflecting the newest information.



34
35
36
37
38
39
40
41
42
43
44
45
# File 'lib/rfd/file_ops.rb', line 34

def ls
  fetch_items_from_filesystem_or_zip
  sort_items_according_to_current_direction

  @current_page ||= 0
  draw_items
  move_cursor (current_row ? [current_row, items.size - 1].min : nil)

  draw_marked_items
  draw_total_items
  true
end

#mkdir(dir) ⇒ Object

Create a new directory.



269
270
271
272
273
274
275
276
277
278
# File 'lib/rfd/file_ops.rb', line 269

def mkdir(dir)
  unless in_zip?
    FileUtils.mkdir_p current_dir.join(dir)
  else
    Zip::File.open(current_zip) do |zip|
      zip.dir.mkdir dir
    end
  end
  ls
end

#mv(dest) ⇒ Object

Move selected files and directories to the destination.



184
185
186
187
188
189
190
191
192
193
# File 'lib/rfd/file_ops.rb', line 184

def mv(dest)
  unless in_zip?
    src = (m = marked_items).any? ? m.map(&:path) : current_item
    FileUtils.mv src, expand_path(dest)
  else
    raise 'mving multiple items in .zip is not supported.' if selected_items.size > 1
    rename "#{selected_items.first.name}/#{dest}"
  end
  ls
end

#pasteObject

Paste yanked files / directories here.



316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
# File 'lib/rfd/file_ops.rb', line 316

def paste
  if @yanked_items
    if current_item.directory?
      FileUtils.cp_r @yanked_items.map(&:path), current_item
    else
      @yanked_items.each do |item|
        if items.include? item
          i = 0
          while i += 1
            new_path = current_dir.join("#{item.basename}_#{i}#{item.extname}")
            break unless File.exist? new_path
          end
          new_item = new_path
          FileUtils.cp_r item, new_item
        else
          FileUtils.cp_r item, current_dir
        end
      end
    end
    ls
  end
end

#popdObject

cd to the previous directory.



28
29
30
# File 'lib/rfd/file_ops.rb', line 28

def popd
  cd @dir_history.pop, pushd: false if @dir_history.any?
end

#rename(pattern) ⇒ Object

Rename selected files and directories.

Parameters

  • pattern - new filename, or a shash separated Regexp like string



199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
# File 'lib/rfd/file_ops.rb', line 199

def rename(pattern)
  from, to = pattern.sub(/^\//, '').sub(/\/$/, '').split '/'
  if to.nil?
    from, to = current_item.name, from
  else
    from = Regexp.new from
  end
  unless in_zip?
    selected_items.each do |item|
      name = item.name.gsub from, to
      FileUtils.mv item, current_dir.join(name) if item.name != name
    end
  else
    Zip::File.open(current_zip) do |zip|
      selected_items.each do |item|
        name = item.name.gsub from, to
        zip.rename item.name, name
      end
    end
  end
  ls
rescue RegexpError => e
  command_line.show_error "Invalid regex: #{e.message}"
end

#sort(direction = nil) ⇒ Object

Sort the whole files and directories in the current directory, then refresh the screen.

Parameters

  • direction - Sort order in a String.

    nil   : order by name
    r     : reverse order by name
    s, S  : order by file size
    sr, Sr: reverse order by file size
    t     : order by mtime
    tr    : reverse order by mtime
    c     : order by ctime
    cr    : reverse order by ctime
    u     : order by atime
    ur    : reverse order by atime
    e     : order by extname
    er    : reverse order by extname
    


63
64
65
66
67
68
69
# File 'lib/rfd/file_ops.rb', line 63

def sort(direction = nil)
  @direction, @current_page = direction, 0
  sort_items_according_to_current_direction
  draw_items
  switch_page 0
  move_cursor
end

#sort_items_according_to_current_directionObject

Sort the loaded files and directories in already given sort order.



127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
# File 'lib/rfd/file_ops.rb', line 127

def sort_items_according_to_current_direction
  dots = items.shift(2)
  reverse = @direction&.end_with?('r')
  key = SORT_KEYS[@direction&.sub(/r$/, '')]

  sorted = items.partition(&:directory?).flat_map do |arr|
    if key
      sorted_arr = arr.sort_by(&key)
      reverse ? sorted_arr : sorted_arr.reverse
    else
      reverse ? arr.sort.reverse : arr.sort
    end
  end

  @items = dots + sorted
  items.each.with_index {|item, index| item.index = index}
end

Create a symlink to the current file or directory.



296
297
298
299
# File 'lib/rfd/file_ops.rb', line 296

def symlink(name)
  FileUtils.ln_s current_item, name
  ls
end

#touch(filename) ⇒ Object

Create a new empty file.



281
282
283
284
285
286
287
288
289
290
291
292
293
# File 'lib/rfd/file_ops.rb', line 281

def touch(filename)
  unless in_zip?
    FileUtils.touch current_dir.join(filename)
  else
    Zip::File.open(current_zip) do |zip|
      # zip.file.open(filename, 'w') {|_f| }  #HAXX this code creates an unneeded temporary file
      zip.instance_variable_get(:@entry_set) << Zip::Entry.new(current_zip, filename)
    end
  end

  ls
  move_cursor items.index {|i| i.name == filename}
end

#touch_t(timestamp) ⇒ Object

Change the timestamp of the selected files and directories.

Parameters

  • timestamp - A string that can be parsed with Time.parse. Note that this parameter is not compatible with UNIX ‘touch -t`.



305
306
307
308
# File 'lib/rfd/file_ops.rb', line 305

def touch_t(timestamp)
  FileUtils.touch selected_items, mtime: Time.parse(timestamp)
  ls
end

#trashObject

Soft delete selected files and directories.

If the OS is not OSX, performs the same as delete command. On macOS, tries the trash command first, then falls back to manual mv to ~/.Trash/.



228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
# File 'lib/rfd/file_ops.rb', line 228

def trash
  unless in_zip?
    if osx?
      paths = selected_items.map(&:path)
      # Try the trash command first (available via Homebrew or other package managers)
      unless system('trash', *paths)
        # Fall back to manual mv to ~/.Trash/
        FileUtils.mv paths, File.expand_path('~/.Trash/')
      end
    else
      #TODO support other OS
      FileUtils.rm_rf selected_items.map(&:path)
    end
  else
    return unless ask %Q[Trashing zip entries is not supported. Actually the files will be deleted. Are you sure want to proceed? (y/n)]
    delete
  end
  @current_row -= selected_items.count {|i| i.index <= current_row}
  ls
end

#unarchiveObject

Unarchive .zip and .tar.gz files within selected files and directories into current_directory.



366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
# File 'lib/rfd/file_ops.rb', line 366

def unarchive
  unless in_zip?
    zips, gzs = selected_items.partition(&:zip?).tap {|z, others| break [z, *others.partition(&:gz?)]}
    zips.each do |item|
      dest_dir = current_dir.join(item.basename)
      FileUtils.mkdir_p dest_dir
      Zip::File.open(item) do |zip|
        zip.each do |entry|
          dest_path = File.join(dest_dir.to_s, entry.name)
          FileUtils.mkdir_p File.dirname(dest_path)
          zip_extract(zip, entry, dest_dir.to_s, dest_path)
        end
      end
    end
    gzs.each do |item|
      Zlib::GzipReader.open(item) do |gz|
        Gem::Package::TarReader.new(gz) do |tar|
          dest_dir = current_dir.join (gz.orig_name || item.basename).sub(/\.tar$/, '')
          tar.each do |entry|
            dest = nil
            if entry.full_name == '././@LongLink'
              dest = safe_extract_path(dest_dir, entry.read.strip)
              next
            end
            dest ||= safe_extract_path(dest_dir, entry.full_name)
            if entry.directory?
              FileUtils.mkdir_p dest, mode: entry.header.mode
            elsif entry.file?
              FileUtils.mkdir_p File.dirname(dest)
              File.open(dest, 'wb') {|f| f.print entry.read}
              FileUtils.chmod entry.header.mode, dest
            elsif entry.header.typeflag == '2'  # symlink
              File.symlink entry.header.linkname, dest
            end
            unless Dir.exist? dest_dir
              FileUtils.mkdir_p dest_dir
              File.open(File.join(dest_dir, gz.orig_name || item.basename), 'wb') {|f| f.print gz.read}
            end
          end
        end
      end
    end
  else
    dest_dir = File.join(current_zip.dir, current_zip.basename)
    FileUtils.mkdir_p dest_dir
    Zip::File.open(current_zip) do |zip|
      zip.select {|e| selected_items.map(&:name).include? e.to_s}.each do |entry|
        dest_path = File.join(dest_dir, entry.name)
        FileUtils.mkdir_p File.dirname(dest_path)
        zip_extract(zip, entry, dest_dir, dest_path)
      end
    end
  end
  ls
end

#yankObject

Yank selected file / directory names.



311
312
313
# File 'lib/rfd/file_ops.rb', line 311

def yank
  @yanked_items = selected_items
end

#zip(zipfile_name) ⇒ Object

Archive selected files and directories into a .zip file.



346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
# File 'lib/rfd/file_ops.rb', line 346

def zip(zipfile_name)
  return unless zipfile_name
  zipfile_name += '.zip' unless zipfile_name.end_with? '.zip'

  zip_file_open_for_create(zipfile_name) do |zipfile|
    selected_items.each do |item|
      next if item.symlink?
      if item.directory?
        Dir[item.join('**/**')].each do |file|
          zipfile.add file.sub("#{current_dir}/", ''), file
        end
      else
        zipfile.add item.name, item
      end
    end
  end
  ls
end