#!/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