Skip to content

Commit

Permalink
[cms][#3241][#4200] copy on write feature for branch (#4201)
Browse files Browse the repository at this point in the history
* [add] attachment lifecycle specs

* [modify] split spec about #new_clone

* [fix] clear "_id" before calling instantiating new object

* [add] more specs about "#new_clone"

* [modify] split specs into individual file

* [modify] share thumb between master and its branches

* [modify] remove deprecated method

* [modify] remove possible dead code

* [modify] remove unused callback "clone_thumb"

* [modify] remove meaningless if

* [modify] share files between master and its branches

* [modify] remove SS::Relation::FileBranch

no need this module

* [modify] reduce duplicated code

* [modify] share column_values' files between master and its branches

* [fix] history/log incompatibilities

* [fix] missing owner_item on new created page with columns

* [fix] for broken thumbnail

* [modify] refactor w/ RuboCop

* [add] spec about restoring branch page from trash

* [add] more specs

* [add] no need to protect creating trash object

* [fix] lost files already existed

* [fixup] share column_values' files between master and its branches

* [fixup] share files between master and its branches

* [fix] spec failures

* [fix] unable to publish save if page has branches

* [modify] safe file owner transfer to restore from trash

* [modify] transfer owner from branch to master before master is saved

* [fix] typo

* [modify] explicitly create thumbnails

* [fixup] more specs

* [fix] able to transfer file onwer item effectively

* [add] SS::File.each_file

* [add] SS::Model.container_of

* [add] SS::File.file_owned?

* [modify] refactor w/ RuboCop

* [add] more history/trash specs

* [fix] unable to restore file if there are no files

* [modify] change destroy files timing, page first and files second

* [add] SS::File.clone_file

* [fix] update html on cloning files

* [modify] add SS::File.create_from_upload

* [modify] reduce duplicated code of ss/relation/file

* [modify] more safe file attachment

* [fix] ArgumentError: missing keyword: static_state

* [modify] refactor ss/relation/file more

* [modify] set model only if it's not presented, and create thumbs

* [modify] clear in_tsv to protect from saving tvs

* [fix] clone file twice

* [fix] several failures

* [modify] revert attributes.to_h

* [fix] file#model issues

* [modify] stabilize spec up
  • Loading branch information
sunny4381 committed Nov 11, 2021
1 parent 985540b commit a601bd5
Show file tree
Hide file tree
Showing 48 changed files with 3,754 additions and 716 deletions.
1 change: 0 additions & 1 deletion app/controllers/concerns/cms/page_filter.rb
Expand Up @@ -220,7 +220,6 @@ def update
save_as_branch
return
end
raise "404" if @item.try(:master?) && @item.branches.present?

publish_save
end
Expand Down
2 changes: 1 addition & 1 deletion app/models/ads/banner.rb
Expand Up @@ -13,7 +13,7 @@ class Ads::Banner
field :link_url, type: String
field :additional_attr, type: Cms::Extensions::HtmlAttributes, default: ""

belongs_to_file2 :file
belongs_to_file :file

validates :link_url, presence: true
validate :validate_link_url
Expand Down
2 changes: 1 addition & 1 deletion app/models/cms/column/value/base.rb
Expand Up @@ -94,7 +94,7 @@ def all_file_ids
end

def clone_to(to_item, opts = {})
attrs = self.attributes.to_h.except('_id').slice(*self.class.fields.keys.map(&:to_s))
attrs = Hash[self.attributes].except('_id').slice(*self.class.fields.keys.map(&:to_s))
ret = to_item.column_values.build(attrs)
ret.instance_variable_set(:@new_clone, true)
ret.instance_variable_set(:@origin_id, self.id)
Expand Down
99 changes: 58 additions & 41 deletions app/models/cms/column/value/file_upload.rb
Expand Up @@ -104,69 +104,86 @@ def file_icon

def before_save_file
if file_id_was.present? && file_id_was != file_id
old_file = SS::File.find(file_id_was) rescue nil
if old_file
old_file.destroy
self.file_id = nil
end
Cms::Addon::File::Utils.delete_files(self, [ file_id_was ]) if file_id_was
end

return if file.blank?

if @new_clone
attributes = Hash[file.attributes]
attributes.select!{ |k| file.fields.key?(k) }
# 注意: カラム処理では以下の点が異なるので注意。
#
# - カラムは数が変更される可能性があるため、master から branch を作成する際も、master へ branch をマージする際も、
# delete & insert となるため常に @new_clone がセットされる。
# - master から branch を作成する際は @merge_values はセットされないのに対し、
# master へ branch をマージする際は @merge_values がセットされる。
# - ゴミ箱から復元する際は、@new_clone も @merge_values もセットされない。
#
# これらの全てのケースに対応する必要がある。
clone_file_if_necessary
update_file_owner_item
end

def clone_file_if_necessary
return unless file

attributes["user_id"] = @cur_user.id if @cur_user
attributes["_id"] = nil
clone_file = SS::File.create_empty!(attributes, validate: false) do |new_file|
::FileUtils.copy(file.path, new_file.path)
end
clone_file.owner_item = _parent
owner_item = SS::Model.container_of(self)
return if SS::File.file_owned?(file, owner_item)

# 差し替えページの場合、ファイルの所有者が差し替え元なら、そのままとする
return if owner_item.try(:branch?) && SS::File.file_owned?(file, owner_item.master)

return unless Cms::Addon::File::Utils.need_to_clone?(file, owner_item, owner_item.try(:in_branch))

cur_user = owner_item.cur_user if owner_item.respond_to?(:cur_user)
new_file = SS::File.clone_file(file, cur_user: cur_user, owner_item: owner_item) do |new_file|
# history_files
if @merge_values
clone_file.master_id = nil
clone_file.history_file_ids = file.history_file_ids
else
clone_file.master_id = file.id
clone_file.history_file_ids = []
new_file.history_file_ids = file.history_file_ids
end

clone_file.save(validate: false)
clone_file.sanitizer_copy_file
self.file = clone_file
end

# サムネイルを作成する
new_file.send(:save_thumbs)

self.file = new_file
self.file_id = new_file.id
end

def update_file_owner_item
owner_item = SS::Model.container_of(self)
return if SS::File.file_owned?(file, owner_item)

# 差し替えページの場合、所有者を差し替え元のままとする
return if owner_item.respond_to?(:branch?) && owner_item.branch? && SS::File.file_owned?(file, owner_item.master)

attrs = {}

if file.site_id != _parent.site_id
attrs[:site_id] = _parent.site_id
if file.site_id != owner_item.site_id
attrs[:site_id] = owner_item.site_id
end
if file.model != _parent.class.name
attrs[:model] = _parent.class.name
if file.model != owner_item.class.name
attrs[:model] = owner_item.class.name
end
if file.owner_item != _parent
attrs[:owner_item] = _parent
if file.owner_item != owner_item
attrs[:owner_item] = owner_item
end
if file.state != _parent.state
attrs[:state] = _parent.state
if file.state != owner_item.state
attrs[:state] = owner_item.state
end

if attrs.present?
file.update(attrs)
return if attrs.blank?

result = file.update(attrs)
if result
History::Log.build_file_log(file, site_id: owner_item.site_id, user_id: owner_item.cur_user.try(:id)).tap do |history|
history.action = "update"
history.behavior = "attachment"
history.save
end
end
end

def destroy_file
return if file.blank?
return nil unless File.exist?(file.path)

path = "#{History::Trash.root}/#{file.path.sub(/.*\/(ss_files\/)/, '\\1')}"
FileUtils.mkdir_p(File.dirname(path))
FileUtils.cp(file.path, path)
file.skip_history_trash = _parent.skip_history_trash if [ _parent, file ].all? { |obj| obj.respond_to?(:skip_history_trash) }
file.destroy
Cms::Addon::File::Utils.delete_files(self, [ file_id ])
end

# override Cms::Column::Value::Base#to_default_html
Expand Down
137 changes: 66 additions & 71 deletions app/models/cms/column/value/free.rb
Expand Up @@ -7,6 +7,7 @@ class Cms::Column::Value::Free < Cms::Column::Value::Base
permit_values :value, file_ids: []

before_validation :set_contains_urls
before_save { @add_file_ids = file_ids - file_ids_was.to_a }
after_save :put_contains_urls_logs
before_parent_save :before_save_files
after_parent_destroy :destroy_files
Expand All @@ -21,13 +22,13 @@ def all_file_ids
end

def generate_public_files
files.each do |file|
SS::File.each_file(file_ids) do |file|
file.generate_public_file
end
end

def remove_public_files
files.each do |file|
SS::File.each_file(file_ids) do |file|
file.remove_public_file
end
end
Expand Down Expand Up @@ -55,92 +56,86 @@ def to_default_html
end

def before_save_files
if @new_clone
cloned_file_ids = []
file_ids.each_slice(20) do |ids|
SS::File.in(id: ids).to_a.each do |source_file|
attributes = Hash[source_file.attributes]
attributes.select!{ |k| source_file.fields.key?(k) }

attributes["user_id"] = @cur_user.id if @cur_user
attributes["_id"] = nil
attributes["model"] = _parent.class.name
attributes["state"] = _parent.state
clone_file = SS::File.create_empty!(attributes, validate: false) do |new_file|
::FileUtils.copy(source_file.path, new_file.path)
end
clone_file.owner_item = _parent

# history_files
if @merge_values
clone_file.master_id = nil
clone_file.history_file_ids = source_file.history_file_ids
else
clone_file.master_id = source_file.id
clone_file.history_file_ids = []
end

clone_file.save(validate: false)
clone_file.sanitizer_copy_file
result = clone_file

next unless result

cloned_file_ids << clone_file.id

cloned_value = self.value.to_s
cloned_value.gsub!("=\"#{source_file.url}\"", "=\"#{clone_file.url}\"")
cloned_value.gsub!("=\"#{source_file.thumb_url}\"", "=\"#{clone_file.thumb_url}\"")
self.value = cloned_value
end
end
# Cms::Addon::File では clone_files をしてから save_files を実行しているので、それに習う。
#
# 注意: カラム処理では以下の点が異なるので注意。
#
# カラムは数が変更される可能性があるため、master から branch を作成する際も、master へ branch をマージする際も、
# delete & insert となるため常に @new_clone がセットされる。
# master から branch を作成する際は @merge_values はセットされないのに対し、
# master へ branch をマージする際は @merge_values がセットされる。
clone_files if @new_clone && !@merge_values
save_files
end

self.file_ids = cloned_file_ids
else
del_ids = file_ids_was.to_a - file_ids
del_ids.each_slice(20) do |ids|
SS::File.in(id: ids).destroy_all
end
def clone_files
return if file_ids.blank?

owner_item = SS::Model.container_of(self)
return if owner_item.respond_to?(:branch?) && owner_item.branch?

add_ids = _parent.state_changed? ? file_ids : file_ids - file_ids_was.to_a
add_ids.each_slice(20) do |ids|
SS::File.in(id: ids).to_a.each do |file|
file.update(site_id: _parent.site_id, model: _parent.class.name, owner_item: _parent, state: _parent.state)
cur_user = owner_item.cur_user if owner_item.respond_to?(:cur_user)
cloned_file_ids = []
SS::File.each_file(file_ids) do |source_file|
clone_file = SS::File.clone_file(source_file, cur_user: cur_user, owner_item: owner_item) do |new_file|
# history_files
if @merge_values
new_file.history_file_ids = source_file.history_file_ids
end
end
next unless clone_file

begin
self.file_ids = file_ids + add_ids - del_ids
rescue
self.file_ids
end
cloned_file_ids << clone_file.id

cloned_value = self.value.to_s
cloned_value.gsub!("=\"#{source_file.url}\"", "=\"#{clone_file.url}\"")
cloned_value.gsub!("=\"#{source_file.thumb_url}\"", "=\"#{clone_file.thumb_url}\"")
self.value = cloned_value
end

self.file_ids = cloned_file_ids
end

def destroy_files
if !_parent.respond_to?(:skip_history_trash)
files.destroy_all
return
end
def save_files
# 追加されたファイルのリストを算出するには before_save でないといけない。
# しかし、before_save コールバックが呼ばれた時点では _parent.id が未確定のため files の owner_item をセットできない。
# 苦肉の策だが before_save コールバックで追加されたファイルのリストを算出し、@add_file_ids に保存する。
# そして、before_parent_save コールバックで files の owner_item をセットする。
owner_item = SS::Model.container_of(self)
in_branch = owner_item.in_branch if @merge_values && owner_item.respond_to?(:in_branch)

file_ids.each_slice(20) do |ids|
SS::File.in(id: ids).to_a.map(&:becomes_with_model).each do |file|
file.skip_history_trash = _parent.skip_history_trash if file.respond_to?(:skip_history_trash)
file.destroy
end
end
on_clone_file = method(:update_value_with_clone_file)
ids = Cms::Addon::File::Utils.attach_files(self, @add_file_ids, branch: in_branch, on_clone_file: on_clone_file)
self.file_ids = ids rescue return

del_ids = file_ids_was.to_a - ids
Cms::Addon::File::Utils.delete_files(self, del_ids)
end

def update_value_with_clone_file(old_file, new_file)
return if value.blank?

value = self.value
value.gsub!("=\"#{old_file.url}\"", "=\"#{new_file.url}\"")
value.gsub!("=\"#{old_file.thumb_url}\"", "=\"#{new_file.thumb_url}\"")
self.value = value
end

def destroy_files
Cms::Addon::File::Utils.delete_files(self, file_ids)
end

def build_history_log(file)
site_id = self._parent.cur_site.id if self._parent.cur_site.present?
user_id = self._parent.cur_user.id if self._parent.cur_user.present?
owner_item = SS::Model.container_of(self)
site_id = owner_item.cur_site.id if owner_item.respond_to?(:cur_site) && owner_item.cur_site
user_id = owner_item.cur_user.id if owner_item.respond_to?(:cur_user) && owner_item.cur_user

History::Log.build_file_log(file, site_id: site_id, user_id: user_id)
end

def put_contains_urls_logs
add_contains_urls = self._parent.value_contains_urls - self._parent.value_contains_urls_was.to_a
owner_item = SS::Model.container_of(self)
add_contains_urls = owner_item.value_contains_urls - owner_item.value_contains_urls_was.to_a
add_contains_urls.each do |file_url|
item = build_history_log(nil)
item.url = file_url
Expand All @@ -150,7 +145,7 @@ def put_contains_urls_logs
item.save
end

del_contains_urls = self._parent.value_contains_urls_was.to_a - self._parent.value_contains_urls
del_contains_urls = owner_item.value_contains_urls_was.to_a - owner_item.value_contains_urls
del_contains_urls.each do |file_url|
item = build_history_log(nil)
item.url = file_url
Expand Down

0 comments on commit a601bd5

Please sign in to comment.