Skip to content

Instantly share code, notes, and snippets.

@fum1h1ro
Last active September 13, 2023 07:16
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save fum1h1ro/dee2570aa81b27e009a9bde1009e0ca1 to your computer and use it in GitHub Desktop.
Save fum1h1ro/dee2570aa81b27e009a9bde1009e0ca1 to your computer and use it in GitHub Desktop.
タイムスタンプじゃなくファイルのハッシュで変更を検知するツール
ヘルプ
$ ruby sync_assets.rb
ビルド&同期
$ ruby sync_assets.rb build -v
クリーン(ハッシュを記録しているフォルダを消す)
$ ruby sync_assets.rb clean
#!/bin/env ruby
require 'thor'
require 'pathname'
require 'digest/md5'
require 'fileutils'
require 'open3'
require 'rake'
def assert(expr, msg="")
unless expr
if msg.empty?
raise RuntimeError.new()
else
raise RuntimeError.new(msg)
end
end
end
class Task
attr_reader :src, :dst
@@image_exts = 'png|jpg'.split('|').map { |x| ".#{x}" }
def initialize(app, src, dst)
@app = app
@src = src
@dst = dst
end
def src_dir
File.dirname(@src)
end
def dst_dir
File.dirname(@dst)
end
def image?
not @@image_exts.find { |x| x == File.extname(src) }.nil?
end
def spine?
'.spine' == File.extname(src)
end
def cp
@app.file_cp(@src, @dst)
end
end
class SyncTask < Task
def resize_cp(scale, suffix="")
assert image?, "#{src} is not image"
dir = Pathname(File.dirname(@dst))
fname = File.basename(@dst, '.*')
ext = File.extname(@dst)
@app.resize_cp(@src, dir.join("#{fname}#{suffix}#{ext}").to_s, scale * 100)
end
end
class ConvTask < Task
def system(cmd)
@app.system(cmd)
end
end
class SyncPair
def initialize(app, src_dir, dst_pattern, block)
@app = app
assert Dir.exists?(src_dir), "#{src_dir} was not found"
@targets = []
src_files = Dir.glob("#{src_dir}/*.*")
if dst_pattern.include?('%')
src_files.each do |src_file|
@targets << [src_file, src_file.pathmap(dst_pattern)]
end
else
dst_dir = dst_pattern
src_files.each do |src_file|
sfn = File.basename(src_file)
@targets << [src_file, "#{dst_dir}/#{sfn}"]
end
end
@block = block
end
def execute(force)
@targets.each do |t|
src = t[0]
dst = t[1]
src_old_hash = @app.get_src_file_hash(src)
src_hash = @app.calc_file_hash(src)
dst_hash = @app.calc_file_hash(dst)
dst_exists = File.exists?(dst)
if force or src_old_hash.nil? or src_old_hash != src_hash or not dst_exists or src_hash != dst_hash
if @app.verbose?
print "sync: #{src} -> #{dst}\n"
print " src old hash: #{src_old_hash}\n" unless src_old_hash.nil?
print " src hash: #{src_hash}\n" unless src_hash.nil?
print " dst hash: #{dst_hash}\n" unless dst_hash.nil?
print " #{dst} was not found.\n" unless dst_exists
end
if @block != nil
@block.call(SyncTask.new(@app, src, dst))
else
SyncTask.new(@app, src, dst).cp
end
@app.set_src_file_hash(src, src_hash)
end
end
end
end
class ConvPair
def initialize(app, src, dst_pattern, block=nil)
@app = app
assert !block.nil?, "no block"
assert File.exists?(src), "#{src} was not found"
@src = src
if dst_pattern.include?('%')
@dst = src.pathmap(dst_pattern)
else
@dst = dst
end
@block = block
end
def execute(force)
src_old_hash = @app.get_src_file_hash(@src)
src_hash = @app.calc_file_hash(@src)
dst_exists = File.exists?(@dst)
if force or src_old_hash.nil? or src_old_hash != src_hash or not dst_exists
if @app.verbose?
print "conv: #{@src} -> #{@dst}\n"
print " hash:#{src_old_hash} #{src_old_hash != src_hash ? '!=' : '=='} #{src_hash}\n" if not src_old_hash.nil? and not src_hash.nil?
print " #{@dst} was not found.\n" unless dst_exists
end
@app.mkdir_if(File.dirname(@dst))
@block.call(ConvTask.new(@app, @src, @dst))
@app.set_src_file_hash(@src, src_hash)
end
end
end
class Application
@@imagemagick_path = '/usr/local/bin/convert'
def initialize(dry: false, verbose: false)
@hash = Digest::MD5.new
@pwd = Dir.pwd
@config = "#{@pwd}/SyncAssets"
@tmp = "#{@pwd}/.sync_assets"
@options = {
:v => verbose,
:n => dry,
}
@syncs = []
@convs = []
@has_imagemagick = File.exists?(@@imagemagick_path)
end
def verbose?
@options[:v]
end
def dry?
@options[:n]
end
def fileutils_options
{
:verbose => verbose?,
:noop => dry?,
}
end
def get_src_file_hash(filename)
hash = @hash.reset.update(File.expand_path(filename))
path = "#{@tmp}/#{hash}"
if File.exists?(path)
File.open(path).read
else
nil
end
end
def set_src_file_hash(filename, filehash)
unless @options[:n]
hash = @hash.reset.update(File.expand_path(filename)).to_s
path = "#{@tmp}/#{hash}"
File.open(path, "w+").print(filehash)
end
end
def calc_file_hash(filename)
if File.exists?(filename)
@hash.reset.file(filename).to_s
else
nil
end
end
def command_clean
$stdout.print("Cleaning temporary data.\n") if verbose?
FileUtils.rm_rf(@tmp, **fileutils_options)
end
def command_build(force)
mkdir_if(@tmp)
if File.exists?(@config)
load @config
end
@syncs.each do |sync|
sync.execute(force)
end
@convs.each do |conv|
conv.execute(force)
end
end
def create_sync(src, dst, block)
@syncs << SyncPair.new(self, src, dst, block)
end
def create_conv(src, dst, block)
@convs << ConvPair.new(self, src, dst, block)
end
def mkdir_if(dir)
FileUtils.mkdir_p(dir, **fileutils_options) unless Dir.exists?(dir)
end
def file_cp(src, dst)
mkdir_if(File.dirname(dst))
FileUtils.cp(src, dst, **fileutils_options)
end
def resize_cp(src, dst, scale)
assert @has_imagemagick, 'no imagemagick'
mkdir_if(File.dirname(dst))
cmd = "#{@@imagemagick_path} \"#{src}\" -resize #{scale}% \"#{dst}\""
system(cmd)
end
def system(cmd)
print "exec: #{cmd}\n" if verbose?
unless dry?
o, e, s = Open3.capture3(cmd)
raise RuntimeError.new(e) unless s.success?
return o
end
end
end
module AppCommand
@@app = nil
class << self
def app
@@app
end
def app=(a)
@@app = a
end
end
def sync(src, dst, &block)
AppCommand.app.create_sync(src, dst, block)
end
def conv(src, dst, &block)
AppCommand.app.create_conv(src, dst, block)
end
end
self.extend AppCommand
class CommandLineInterface < Thor
#default_command :build
class_option 'dry-run', :aliases => '-n', :type => :boolean, :default => false
class_option 'verbose', :aliases => '-v', :type => :boolean, :default => false
desc "clean", "clean temporary data"
def clean()
AppCommand.app = Application.new(verbose: @options[:verbose], dry: @options['dry-run'])
AppCommand.app.command_clean
end
desc "build [-f|--force]", "build & sync assets"
method_option :force, :aliases => '-f', :type => :boolean, :default => false, :desc => "Force"
def build()
AppCommand.app = Application.new(verbose: @options[:verbose], dry: @options['dry-run'])
AppCommand.app.command_build(options[:force])
end
end
CommandLineInterface.start(ARGV)
DROPBOX_ROOT = File.expand_path("~/Hoge Dropbox") + "/hoge"
PROJECT_ROOT = "Unity"
ASSETS_ROOT = "#{PROJECT_ROOT}/Assets"
ADDRESSABLE_RESOURCES_ROOT = "#{ASSETS_ROOT}/AddressableResources"
REFERENCED_RESOURCES_ROOT = "#{ASSETS_ROOT}/ReferencedResources"
SPRITES_ROOT = "#{REFERENCED_RESOURCES_ROOT}/Sprites"
BACKGROUND_ROOT = "#{REFERENCED_RESOURCES_ROOT}/Background"
SPINE_CHARACTER_ROOT = "#{REFERENCED_RESOURCES_ROOT}/SpineResources/Characters"
SPINE_EFFECT_ROOT = "#{REFERENCED_RESOURCES_ROOT}/SpineResources/Effects"
SPINE_MAP_ROOT = "#{REFERENCED_RESOURCES_ROOT}/SpineMapResources"
SPINE_EXPORTER = "ruby Tools/spine_exporter.rb"
SOUND_ROOT = "#{ADDRESSABLE_RESOURCES_ROOT}/Sound"
# 左のフォルダの中のファイルを右のフォルダへ同期(ハッシュチェックあり)
sync File.expand_path("~/Documents/GitHub/hoge-config"), "#{PROJECT_ROOT}/Config" do |s|
s.cp
end
# 上と同じだけどコピー先のファイル名をpathmapしてくれる
sync "#{DROPBOX_ROOT}/LargeImages", "#{ADDRESSABLE_RESOURCES_ROOT}/LargeImages/%f.bytes" do |s|
s.cp
end
Dir.glob("#{DROPBOX_ROOT}/BG/**/").each do |src|
dir = File.basename(src)
next if dir.start_with?('_')
dst = src.gsub("#{DROPBOX_ROOT}/BG", "#{BACKGROUND_ROOT}")
sync src, dst do |s|
s.cp
# 画像ファイルは拡縮してコピーできる
[4, 8].each do |x|
s.resize_cp(1.0 / x, "@#{x}x")
end
end
end
# コンバート
Dir.glob("#{DROPBOX_ROOT}/SpineData/character/*/*.spine").each do |src|
conv src, "#{SPINE_CHARACTER_ROOT}/%n/%n.json" do |c|
c.system("#{SPINE_EXPORTER} \"#{c.src}\" -o \"#{c.dst_dir}\" -e \"Settings/hoge.export.json\"")
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment