Newer
Older
nextcloud_move_old_files / move_old_files.rb
#!/usr/bin/env ruby
# coding: utf-8
require 'bundler/setup'
require 'net/http'
require 'uri'
require 'time'
require 'nokogiri'
require 'dotenv'
Dotenv.load

# Net::HTTP に PROPFIND と MOVE を定義(未定義の場合のみ)
unless Net::HTTP.const_defined?('Propfind')
  class Net::HTTP::Propfind < Net::HTTPRequest
    METHOD = 'PROPFIND'
    REQUEST_HAS_BODY = true
    RESPONSE_HAS_BODY = true
  end
end

unless Net::HTTP.const_defined?('Move')
  class Net::HTTP::Move < Net::HTTPRequest
    METHOD = 'MOVE'
    REQUEST_HAS_BODY = false
    RESPONSE_HAS_BODY = true
  end
end

# ===== 設定値 =====
BASE_URL   = ENV['BASE_URL'] || 'https://next.omhouse.mydns.jp/'  # Nextcloud ベース URL
NC_USER    = ENV['NC_USER'] || 'syuji'
# NC_PASS は .env または環境変数で設定してください(このファイルに平文で書かないでください)
NC_PASS    = ENV['NC_PASS']
TARGET_DIR = '/Public'         # WebDAV 上の対象フォルダ
BAK_DIR    = '/Public/bak'    # 移動先フォルダ(あらかじめ作成しておく)

# 7日前より古いファイルを移動対象とする
LIMIT_TIME = Time.now - 7 * 24 * 60 * 60

# PROPFIND で取得したいプロパティを XML で定義
PROPFIND_BODY = <<~XML
<?xml version="1.0" encoding="utf-8" ?>
<d:propfind xmlns:d="DAV:">
  <d:prop>
    <d:getlastmodified/>
  </d:prop>
</d:propfind>
XML

def propfind(path, depth = 1)
  # path は "/Public" のように先頭にスラッシュを含む想定
  uri = URI.parse(BASE_URL.chomp('/') + "/remote.php/dav/files/#{NC_USER}" + path)
  req = Net::HTTP::Propfind.new(uri.request_uri)
  req['Depth'] = depth.to_s
  req['Content-Type'] = 'text/xml'
  req.body = PROPFIND_BODY
  req.basic_auth(NC_USER, NC_PASS)

  Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == 'https') do |http|
    http.request(req)
  end
end

def move_file(src_href, dest_filename)
  src_uri  = URI.parse(BASE_URL.chomp('/') + src_href)
  dest_path = BAK_DIR + '/' + dest_filename
  dest_uri = URI.parse(BASE_URL.chomp('/') + "/remote.php/dav/files/#{NC_USER}" + dest_path)
  req = Net::HTTP::Move.new(src_uri.request_uri)
  req['Destination'] = dest_uri.to_s
  req['Overwrite']   = 'T'
  req.basic_auth(NC_USER, NC_PASS)
  Net::HTTP.start(src_uri.host, src_uri.port, use_ssl: src_uri.scheme == 'https') do |http|
    http.request(req)
  end
end

def fetch_share_id(file_path)
  api_uri = URI.parse(BASE_URL.chomp('/') + "/ocs/v2.php/apps/files_sharing/api/v1/shares")
  api_uri.query = URI.encode_www_form(path: file_path)
  req = Net::HTTP::Get.new(api_uri.request_uri)
  req['OCS-APIRequest'] = 'true'
  req.basic_auth(NC_USER, NC_PASS)
  res = Net::HTTP.start(api_uri.host, api_uri.port, use_ssl: api_uri.scheme == 'https') do |http|
    http.request(req)
  end
  return nil unless [100,200,207].include?(res.code.to_i)
  doc = Nokogiri::XML(res.body)
  id_elem = doc.at_xpath('//id')
  id_elem.text if id_elem
end

def delete_share(share_id)
  del_uri = URI.parse(BASE_URL.chomp('/') + "/ocs/v2.php/apps/files_sharing/api/v1/shares/#{share_id}")
  req = Net::HTTP::Delete.new(del_uri.request_uri)
  req['OCS-APIRequest'] = 'true'
  req.basic_auth(NC_USER, NC_PASS)
  Net::HTTP.start(del_uri.host, del_uri.port, use_ssl: del_uri.scheme == 'https') do |http|
    http.request(req)
  end
end

def process_files
  res = propfind(TARGET_DIR, 1)
  return unless res.code.to_i == 207

  moved_count = 0
  dry_count = 0
  deleted_shares_count = 0

  doc = Nokogiri::XML(res.body)
  doc.remove_namespaces!
  doc.xpath('//response').each do |resp|
    href = resp.at_xpath('href')&.text
    next unless href
    # 元ディレクトリ自身はスキップ
    next if href.chomp('/').end_with?(TARGET_DIR.chomp('/'))
    lastmod = resp.at_xpath('propstat/prop/getlastmodified')&.text
    puts "Found resource: #{href}, lastmod: #{lastmod}"
    next unless lastmod

    begin
      file_time = Time.httpdate(lastmod)
    rescue
      next
    end
    next unless file_time < LIMIT_TIME

    # href から WebDAV パスを取り出す
    path_part = href.split("/remote.php/dav/files/#{NC_USER}", 2)[1]
    next unless path_part
    # bak フォルダおよびその配下は除外
    if path_part.start_with?(BAK_DIR)
      next
    end

    filename = path_part.split('/').last
    share_id = fetch_share_id(path_part)
    if share_id
      if defined?(DRY_RUN) && DRY_RUN
        puts "DRY RUN: would delete share #{share_id} for #{path_part}"
        deleted_shares_count += 1
      else
        delete_share(share_id)
        deleted_shares_count += 1
      end
    end

    if defined?(DRY_RUN) && DRY_RUN
      puts "DRY RUN: would move #{href} -> #{BAK_DIR}/#{filename}"
      dry_count += 1
    else
      move_file(href, filename)
      moved_count += 1
    end
  end
  puts "\nSummary:"
  if defined?(DRY_RUN) && DRY_RUN
    puts "  would-move: #{dry_count} files"
  else
    puts "  moved: #{moved_count} files"
  end
  puts "  shares-deleted: #{deleted_shares_count}"
end


if __FILE__ == $0
  # コマンドラインオプションの簡易パース
  if ARGV.include?('--dry-run') || ARGV.include?('-n')
    DRY_RUN = true
  else
    DRY_RUN = false
  end

  process_files
end