filecluster 0.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,12 @@
1
+ class CheckThread < BaseThread
2
+ def go(storage_name)
3
+ $log.debug("CheckThread: Run stotage check for #{storage_name}")
4
+ storage = $storages.detect{|s| s.name == storage_name}
5
+ if File.writable?(storage.path)
6
+ storage.update_check_time
7
+ else
8
+ error "Storage #{storage.name} with path #{storage.path} not writable"
9
+ end
10
+ $log.debug("CheckThread: Finish stotage check for #{storage_name}")
11
+ end
12
+ end
@@ -0,0 +1,60 @@
1
+ class GlobalDaemonThread < BaseThread
2
+ def go(timeout)
3
+ $log.info("Start global daemon thread with timeout=#{timeout}")
4
+ while true do
5
+ exit if $exit_signal
6
+ sleep timeout.to_f/2
7
+ exit if $exit_signal
8
+
9
+ r = FC::DB.connect.query("SELECT #{FC::DB.prefix}vars.*, UNIX_TIMESTAMP() as curr_time FROM #{FC::DB.prefix}vars WHERE name='global_daemon_host'").first
10
+ if r['val'] == FC::Storage.curr_host
11
+ FC::DB.connect.query("UPDATE #{FC::DB.prefix}vars SET val='#{FC::Storage.curr_host}' WHERE name='global_daemon_host'")
12
+ else
13
+ $log.info("Exit from GlobalDaemonThread: global daemon already running on #{r['val']}")
14
+ FC::DB.close
15
+ exit
16
+ end
17
+
18
+ make_item_copies
19
+
20
+ #периодическая проверка на item со статусом delete, последним обновлением дольше суток (NOW - time > 86400) и без связанных is - удаление таких из базы
21
+ #периодически удалять (проставлять статус delete) для лиших is (число копий больше необходимого)
22
+ end
23
+ end
24
+
25
+ # make item copies by policy
26
+ def make_item_copies
27
+ $log.debug("GlobalDaemonThread: make_item_copies")
28
+
29
+ all_storages = FC::Storage.where
30
+ all_policies = FC::Policy.where
31
+
32
+ # policies.get_storages => all_policies.select
33
+ all_policies.each do |policy|
34
+ metaclass = class << policy; self; end
35
+ metaclass.send(:define_method, :get_storages) do
36
+ policy_storages = self.storages.split(',')
37
+ all_storages.select{|storage| policy_storages.member?(storage.name)}
38
+ end
39
+ end
40
+
41
+ sql = "SELECT i.id as item_id, i.size, i.copies as item_copies, GROUP_CONCAT(ist.storage_name) as storages, p.id as policy_id, p.copies as policy_copies "+
42
+ "FROM #{FC::Item.table_name} as i, #{FC::Policy.table_name} as p, #{FC::ItemStorage.table_name} as ist WHERE "+
43
+ "i.policy_id = p.id AND ist.item_id = i.id AND i.copies > 0 AND i.copies < p.copies AND i.status = 'ready' AND ist.status <> 'delete' GROUP BY i.id LIMIT 1000"
44
+ r = FC::DB.connect.query(sql)
45
+ r.each do |row|
46
+ $log.info("GlobalDaemonThread: new item_storage for item #{row['item_id']}")
47
+ item_storages = row['storages'].split(',')
48
+ if row['item_copies'] != item_storages.size
49
+ $log.warn("GlobalDaemonThread: ItemStorage count <> item.copies for item #{row['item_id']}")
50
+ elsif item_storages.size >= row['policy_copies']
51
+ $log.warn("GlobalDaemonThread: ItemStorage count >= policy.copies for item #{row['item_id']}")
52
+ else
53
+ policy = all_policies.detect{|p| row['policy_id'] == p.id}
54
+ storage = policy.get_proper_storage(row['size'], item_storages) if policy
55
+ error 'No available storage', :item_id => row['item_id'] unless storage
56
+ FC::Item.new(:id => row['item_id']).make_item_storage(storage, 'copy')
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,37 @@
1
+ class TaskThread < BaseThread
2
+ def go(storage_name)
3
+ while task = $tasks[storage_name].shift do
4
+ $curr_task[storage_name] = task
5
+ $log.debug("TaskThread(#{storage_name}): run task type=#{task[:action]}, item_storage=#{task[:item_storage].id}")
6
+ if task[:action] == :delete
7
+ make_delete(task[:item_storage])
8
+ elsif task[:action] == :copy
9
+ make_copy(task[:item_storage])
10
+ else
11
+ error "Unknown task action: #{task[:action]}"
12
+ end
13
+ $curr_task[storage_name] = nil
14
+ $log.debug("TaskThread(#{storage_name}): Finish task type=#{task[:action]}, item_storage=#{task[:item_storage].id}")
15
+ end
16
+ end
17
+
18
+ def make_delete(item_storage)
19
+ storage = $storages.detect{|s| s.name == item_storage.storage_name}
20
+ item = FC::Item.find(item_storage.item_id)
21
+ storage.delete_file(item.name)
22
+ item_storage.delete
23
+ rescue Exception => e
24
+ error "Delete item_storage error: #{e.message}; #{e.backtrace.join(', ')}", :item_id => item_storage.item_id, :item_storage_id => item_storage.id
25
+ end
26
+
27
+ def make_copy(item_storage)
28
+ # TODO: не лазить в базу за item, item_storages - перенести на стадию подготовки task-а
29
+ storage = $storages.detect{|s| s.name == item_storage.storage_name}
30
+ item = FC::Item.find(item_storage.item_id)
31
+ src_item_storage = FC::ItemStorage.where("item_id = ? AND status = 'ready'", item.id).sample
32
+ src_storage = $storages.detect{|s| s.name == src_item_storage.storage_name}
33
+ item.copy_item_storage(src_storage, storage, item_storage)
34
+ rescue Exception => e
35
+ error "Copy item_storage error: #{e.message}; #{e.backtrace.join(', ')}", :item_id => item_storage.item_id, :item_storage_id => item_storage.id
36
+ end
37
+ end
data/lib/fc/base.rb ADDED
@@ -0,0 +1,82 @@
1
+ # encoding: utf-8
2
+
3
+ module FC
4
+ class DbBase
5
+ attr_accessor :id, :database_fields
6
+
7
+ class << self
8
+ attr_accessor :table_name, :table_fields
9
+ end
10
+
11
+ def initialize(params = {})
12
+ self.class.table_fields.each {|key| self.send("#{key}=", params[key] || params[key.to_sym]) }
13
+ @id = (params["id"] || params[:id]).to_i if params["id"] || params[:id]
14
+ @database_fields = params[:database_fields] || {}
15
+ end
16
+
17
+ # define table name and fields
18
+ def self.set_table(name, fields)
19
+ self.table_name = "#{FC::DB.prefix}#{name}"
20
+ self.table_fields = fields.split(',').map{|e| e.gsub(' ','')}
21
+ self.table_fields.each{|e| attr_accessor e.to_sym}
22
+ end
23
+
24
+ # make instance on fields hash
25
+ def self.create_from_fiels(data)
26
+ # use only defined in set_table fields
27
+ database_fields = data.select{|key, val| self.table_fields.include?(key.to_s)}
28
+ self.new(database_fields.merge({:id => data["id"].to_s, :database_fields => database_fields}))
29
+ end
30
+
31
+ # get element by id
32
+ def self.find(id)
33
+ r = FC::DB.connect.query("SELECT * FROM #{self.table_name} WHERE id=#{id.to_i}")
34
+ raise "Record not found (#{self.table_name}.id=#{id})" if r.count == 0
35
+ self.create_from_fiels(r.first)
36
+ end
37
+
38
+ # get elements array by sql condition (possibles '?' placeholders)
39
+ def self.where(cond = "1", *params)
40
+ i = -1
41
+ sql = "SELECT * FROM #{self.table_name} WHERE #{cond.gsub('?'){i+=1; "'#{Mysql2::Client.escape(params[i].to_s)}'"}}"
42
+ r = FC::DB.connect.query(sql)
43
+ r.map{|data| self.create_from_fiels(data)}
44
+ end
45
+
46
+ # save changed fields
47
+ def save
48
+ sql = @id.to_i != 0 ? "UPDATE #{self.class.table_name} SET " : "INSERT IGNORE INTO #{self.class.table_name} SET "
49
+ fields = []
50
+ self.class.table_fields.each do |key|
51
+ db_val = @database_fields[key]
52
+ val = self.send(key)
53
+ if val.to_s != db_val.to_s || val.nil? && !db_val.nil? || !val.nil? && db_val.nil?
54
+ fields << "#{key}=#{val ? (val.class == String ? "'#{FC::DB.connect.escape(val)}'" : val.to_i) : 'NULL'}"
55
+ end
56
+ end
57
+ if fields.length > 0
58
+ sql << fields.join(',')
59
+ sql << " WHERE id=#{@id.to_i}" if @id
60
+ FC::DB.connect.query(sql)
61
+ @id = FC::DB.connect.last_id unless @id
62
+ self.class.table_fields.each do |key|
63
+ @database_fields[key] = self.send(key)
64
+ end
65
+ end
66
+ end
67
+
68
+ # reload object from DB
69
+ def reload
70
+ raise "Can't reload object without id" if !@id || @id.to_i == 0
71
+ new_obj = self.class.find(@id)
72
+ self.database_fields = new_obj.database_fields
73
+ self.class.table_fields.each {|key| self.send("#{key}=", new_obj.send(key)) }
74
+ end
75
+
76
+ # delete object from DB
77
+ def delete
78
+ FC::DB.connect.query("DELETE FROM #{self.class.table_name} WHERE id=#{@id.to_i}") if @id
79
+ end
80
+
81
+ end
82
+ end
data/lib/fc/db.rb ADDED
@@ -0,0 +1,168 @@
1
+ require 'mysql2'
2
+
3
+ module FC
4
+ module DB
5
+ class << self; attr_accessor :options, :prefix end
6
+
7
+ def DB.connect_by_config(options)
8
+ @options = options.clone
9
+ @options[:port] = options[:port].to_i if options[:port]
10
+ @connects = {}
11
+ @connects[Thread.current.object_id] = Mysql2::Client.new(@options)
12
+ @prefix = options[:prefix].to_s
13
+ end
14
+
15
+ def self.connect
16
+ if @options[:multi_threads]
17
+ @connects[Thread.current.object_id] ||= Mysql2::Client.new(@options)
18
+ else
19
+ @connects.first[1]
20
+ end
21
+ end
22
+
23
+ def self.close
24
+ if @options[:multi_threads]
25
+ if @connects[Thread.current.object_id]
26
+ @connects[Thread.current.object_id].close
27
+ @connects.delete(Thread.current.object_id)
28
+ end
29
+ else
30
+ @connects.first[1].close
31
+ end
32
+ end
33
+
34
+ def DB.init_db
35
+ FC::DB.connect.query(%{
36
+ CREATE TABLE #{@prefix}items (
37
+ id int NOT NULL AUTO_INCREMENT,
38
+ name varchar(1024) NOT NULL DEFAULT '',
39
+ tag varchar(255) DEFAULT NULL,
40
+ outer_id int DEFAULT NULL,
41
+ policy_id int NOT NULL,
42
+ dir tinyint(1) NOT NULL DEFAULT 0,
43
+ size bigint NOT NULL DEFAULT 0,
44
+ status ENUM('new', 'ready', 'error', 'delete') NOT NULL DEFAULT 'new',
45
+ time int DEFAULT NULL,
46
+ copies int NOT NULL DEFAULT 0,
47
+ PRIMARY KEY (id), UNIQUE KEY (name(255), policy_id),
48
+ KEY (outer_id), KEY (time, status), KEY (status), KEY (copies, status, policy_id)
49
+ ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci
50
+ })
51
+ proc_time = %{
52
+ SET NEW.time = UNIX_TIMESTAMP();
53
+ }
54
+ FC::DB.connect.query("CREATE TRIGGER fc_items_before_insert BEFORE INSERT on #{@prefix}items FOR EACH ROW BEGIN #{proc_time} END")
55
+ FC::DB.connect.query("CREATE TRIGGER fc_items_before_update BEFORE UPDATE on #{@prefix}items FOR EACH ROW BEGIN #{proc_time} END")
56
+
57
+ FC::DB.connect.query(%{
58
+ CREATE TABLE #{@prefix}storages (
59
+ id int NOT NULL AUTO_INCREMENT,
60
+ name varchar(255) NOT NULL DEFAULT '',
61
+ host varchar(255) NOT NULL DEFAULT '',
62
+ path text NOT NULL DEFAULT '',
63
+ url text NOT NULL DEFAULT '',
64
+ size bigint NOT NULL DEFAULT 0,
65
+ size_limit bigint NOT NULL DEFAULT 0,
66
+ check_time int DEFAULT NULL,
67
+ PRIMARY KEY (id), UNIQUE KEY (name), KEY (host)
68
+ ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci
69
+ })
70
+ proc = %{
71
+ # update policy.storages on storage delete and update
72
+ UPDATE #{@prefix}policies,
73
+ (SELECT #{@prefix}policies.id, GROUP_CONCAT(name) as storages FROM #{@prefix}policies LEFT JOIN #{@prefix}storages ON FIND_IN_SET(name, storages) GROUP BY #{@prefix}policies.id) as new_policy
74
+ SET #{@prefix}policies.storages = new_policy.storages WHERE #{@prefix}policies.id = new_policy.id;
75
+ }
76
+ proc_update = %{
77
+ IF OLD.name <> NEW.name THEN
78
+ #{proc}
79
+ END IF;
80
+ }
81
+ FC::DB.connect.query("CREATE TRIGGER fc_storages_after_delete AFTER DELETE on #{@prefix}storages FOR EACH ROW BEGIN #{proc} END")
82
+ FC::DB.connect.query("CREATE TRIGGER fc_storages_after_update AFTER UPDATE on #{@prefix}storages FOR EACH ROW BEGIN #{proc_update} END")
83
+
84
+ FC::DB.connect.query(%{
85
+ CREATE TABLE #{@prefix}policies (
86
+ id int NOT NULL AUTO_INCREMENT,
87
+ storages text NOT NULL DEFAULT '',
88
+ copies int NOT NULL DEFAULT 0,
89
+ PRIMARY KEY (id)
90
+ ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci
91
+ })
92
+ proc = %{
93
+ # update policy.storages on policy change - guarantee valid policy.storages
94
+ SELECT GROUP_CONCAT(name) INTO @storages_list FROM #{@prefix}storages WHERE FIND_IN_SET(name, NEW.storages);
95
+ SET NEW.storages = @storages_list;
96
+ }
97
+ FC::DB.connect.query("CREATE TRIGGER fc_policies_before_insert BEFORE INSERT on #{@prefix}policies FOR EACH ROW BEGIN #{proc} END")
98
+ FC::DB.connect.query("CREATE TRIGGER fc_policies_before_update BEFORE UPDATE on #{@prefix}policies FOR EACH ROW BEGIN #{proc} END")
99
+
100
+ FC::DB.connect.query(%{
101
+ CREATE TABLE #{@prefix}items_storages (
102
+ id int NOT NULL AUTO_INCREMENT,
103
+ item_id int DEFAULT NULL,
104
+ storage_name varchar(255) DEFAULT NULL,
105
+ status ENUM('new', 'copy', 'error', 'ready', 'delete') NOT NULL DEFAULT 'new',
106
+ time int DEFAULT NULL,
107
+ PRIMARY KEY (id), UNIQUE KEY (item_id, storage_name), KEY (storage_name), KEY (time, status), KEY (status, storage_name),
108
+ FOREIGN KEY (item_id) REFERENCES #{@prefix}items(id) ON UPDATE RESTRICT ON DELETE RESTRICT,
109
+ FOREIGN KEY (storage_name) REFERENCES #{@prefix}storages(name) ON UPDATE RESTRICT ON DELETE RESTRICT
110
+ ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci
111
+ })
112
+ proc = %{
113
+ SELECT status, copies, size INTO @item_status, @item_copies, @item_size FROM #{@prefix}items WHERE id = NEW.item_id;
114
+ SET @curr_copies = (SELECT count(*) FROM #{@prefix}items_storages WHERE item_id = NEW.item_id AND status <> 'delete');
115
+ SET @curr_copies_ready = (SELECT count(*) FROM #{@prefix}items_storages WHERE item_id = NEW.item_id AND status = 'ready');
116
+ # calc item.copies
117
+ IF @curr_copies <> @item_copies THEN
118
+ UPDATE #{@prefix}items SET copies=@curr_copies WHERE id = NEW.item_id;
119
+ END IF;
120
+ # check error status
121
+ IF @item_status <> 'new' AND @item_status <> 'delete' AND @curr_copies_ready = 0 THEN
122
+ UPDATE #{@prefix}items SET status='error' WHERE id = NEW.item_id;
123
+ END IF;
124
+ # check ready status
125
+ IF @curr_copies_ready > 0 THEN
126
+ UPDATE #{@prefix}items SET status='ready' WHERE id = NEW.item_id;
127
+ END IF;
128
+ }
129
+ proc_add = %{
130
+ #{proc}
131
+ UPDATE #{@prefix}storages SET size=size+@item_size WHERE name = NEW.storage_name;
132
+ }
133
+ proc_del = %{
134
+ #{proc.gsub('NEW', 'OLD')}
135
+ UPDATE #{@prefix}storages SET size=size-@item_size WHERE name = OLD.storage_name;
136
+ }
137
+ FC::DB.connect.query("CREATE TRIGGER fc_items_storages_before_insert BEFORE INSERT on #{@prefix}items_storages FOR EACH ROW BEGIN #{proc_time} END")
138
+ FC::DB.connect.query("CREATE TRIGGER fc_items_storages_before_update BEFORE UPDATE on #{@prefix}items_storages FOR EACH ROW BEGIN #{proc_time} END")
139
+ FC::DB.connect.query("CREATE TRIGGER fc_items_storages_after_update AFTER UPDATE on #{@prefix}items_storages FOR EACH ROW BEGIN #{proc} END")
140
+ FC::DB.connect.query("CREATE TRIGGER fc_items_storages_after_insert AFTER INSERT on #{@prefix}items_storages FOR EACH ROW BEGIN #{proc_add} END")
141
+ FC::DB.connect.query("CREATE TRIGGER fc_items_storages_after_delete AFTER DELETE on #{@prefix}items_storages FOR EACH ROW BEGIN #{proc_del} END")
142
+
143
+ FC::DB.connect.query(%{
144
+ CREATE TABLE #{@prefix}errors (
145
+ id int NOT NULL AUTO_INCREMENT,
146
+ item_id int DEFAULT NULL,
147
+ item_storage_id int DEFAULT NULL,
148
+ host varchar(255) DEFAULT NULL,
149
+ message varchar(255) DEFAULT NULL,
150
+ time int DEFAULT NULL,
151
+ PRIMARY KEY (id), KEY (item_id), KEY (item_storage_id), KEY (host), KEY (time)
152
+ ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci
153
+ })
154
+ FC::DB.connect.query("CREATE TRIGGER fc_errors_before_insert BEFORE INSERT on #{@prefix}errors FOR EACH ROW BEGIN #{proc_time} END")
155
+
156
+ FC::DB.connect.query(%{
157
+ CREATE TABLE #{@prefix}vars (
158
+ name varchar(255) DEFAULT NULL,
159
+ val varchar(255) DEFAULT NULL,
160
+ time int DEFAULT NULL,
161
+ PRIMARY KEY (name)
162
+ ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci
163
+ })
164
+ FC::DB.connect.query("CREATE TRIGGER fc_vars_before_insert BEFORE INSERT on #{@prefix}vars FOR EACH ROW BEGIN #{proc_time} END")
165
+ FC::DB.connect.query("CREATE TRIGGER fc_vars_before_update BEFORE UPDATE on #{@prefix}vars FOR EACH ROW BEGIN #{proc_time} END")
166
+ end
167
+ end
168
+ end
data/lib/fc/error.rb ADDED
@@ -0,0 +1,12 @@
1
+ # encoding: utf-8
2
+
3
+ module FC
4
+ class Error < DbBase
5
+ set_table :errors, 'item_id, item_storage_id, host, message, time'
6
+
7
+ def self.raise(error, options = {})
8
+ self.new(options.merge(:host => FC::Storage.curr_host, :message => error)).save
9
+ Kernel.raise error
10
+ end
11
+ end
12
+ end
data/lib/fc/item.rb ADDED
@@ -0,0 +1,102 @@
1
+ # encoding: utf-8
2
+
3
+ module FC
4
+ class Item < DbBase
5
+ set_table :items, 'name, tag, outer_id, policy_id, dir, size, status, time, copies'
6
+
7
+ # create item by local path
8
+ # TODO проверка curr_host и local_path одному из доступных стораджей -> создание без копирования (для кусочков)
9
+ def self.create_from_local(local_path, item_name, policy, options={})
10
+ raise 'Path not exists' unless File.exists?(local_path)
11
+ raise 'Policy is not FC::Policy' unless policy.instance_of?(FC::Policy)
12
+ item_params = options.merge({
13
+ :name => item_name.to_s.gsub('//', '/').sub(/\/$/, '').sub(/^\//, '').strip,
14
+ :policy_id => policy.id,
15
+ :dir => File.directory?(local_path),
16
+ :size => `du -sb #{local_path}`.to_i
17
+ })
18
+ item_params.delete(:replace)
19
+ raise 'Name is empty' if item_params[:name].empty?
20
+ raise 'Zero size path' if item_params[:size] == 0
21
+
22
+ # new item?
23
+ item = FC::Item.where('name=? AND policy_id=?', item_params[:name], policy.id).first
24
+ if item
25
+ if options[:replace]
26
+ # mark delete item_storages on replace
27
+ FC::DB.connect.query("UPDATE #{FC::ItemStorage.table_name} SET status='delete' WHERE item_id = #{item.id}")
28
+ # replace all fields
29
+ item_params.each{|key, val| item.send("#{key}=", val)}
30
+ else
31
+ FC::Error.raise 'Item already exists', :item_id => item.id
32
+ end
33
+ else
34
+ item = FC::Item.new(item_params)
35
+ end
36
+ item.save
37
+
38
+ storage = policy.get_proper_storage(item.size)
39
+ FC::Error.raise 'No available storage', :item_id => item.id unless storage
40
+
41
+ item_storage = item.make_item_storage(storage)
42
+ item.copy_item_storage(local_path, storage, item_storage)
43
+ return item
44
+ end
45
+
46
+ def make_item_storage(storage, status = 'new')
47
+ # new storage_item?
48
+ item_storage = FC::ItemStorage.where('item_id=? AND storage_name=?', id, storage.name).first
49
+ item_storage.delete if item_storage
50
+
51
+ item_storage = FC::ItemStorage.new({:item_id => id, :storage_name => storage.name, :status => status})
52
+ item_storage.save
53
+ item_storage
54
+ end
55
+
56
+ def copy_item_storage(src, storage, item_storage)
57
+ begin
58
+ if src.instance_of?(FC::Storage)
59
+ src.copy_to_local(name, "#{storage.path}#{name}")
60
+ else
61
+ storage.copy_path(src, name)
62
+ end
63
+ size_on_storage = storage.file_size(name)
64
+ rescue Exception => e
65
+ item_storage.status = 'error'
66
+ item_storage.save
67
+ FC::Error.raise "Copy error: #{e.message}", :item_id => id, :item_storage_id => item_storage.id
68
+ else
69
+ begin
70
+ item_storage.reload
71
+ rescue Exception => e
72
+ FC::Error.raise "After copy error: #{e.message}", :item_id => id, :item_storage_id => item_storage.id
73
+ else
74
+ if size_on_storage != size
75
+ item_storage.status = 'error'
76
+ item_storage.save
77
+ FC::Error.raise "Check size after copy error", :item_id => id, :item_storage_id => item_storage.id
78
+ else
79
+ item_storage.status = 'ready'
80
+ item_storage.save
81
+ reload
82
+ end
83
+ end
84
+ end
85
+ end
86
+
87
+ # mark items_storages for delete
88
+ def mark_deleted
89
+ FC::DB.connect.query("UPDATE #{FC::ItemStorage.table_name} SET status='delete' WHERE item_id = #{id}")
90
+ self.status = 'delete'
91
+ save
92
+ end
93
+
94
+ def dir?
95
+ dir.to_i == 1
96
+ end
97
+
98
+ def get_item_storages
99
+ FC::ItemStorage.where("item_id = #{id}")
100
+ end
101
+ end
102
+ end