rush 0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,141 @@
1
+ # Rush::Entry is the base class for Rush::File and Rush::Dir. One or more of
2
+ # these is instantiated whenever you use square brackets to access the
3
+ # filesystem on a box, as well as any other operation that returns an entry or
4
+ # list of entries.
5
+ class Rush::Entry
6
+ attr_reader :box, :name, :path
7
+
8
+ # Initialize with full path to the file or dir, and the box it resides on.
9
+ def initialize(full_path, box=nil)
10
+ full_path = ::File.expand_path(full_path, '/')
11
+ @path = ::File.dirname(full_path)
12
+ @name = ::File.basename(full_path)
13
+ @box = box || Rush::Box.new('localhost')
14
+ end
15
+
16
+ # The factory checks to see if the full path has a trailing slash for
17
+ # creating a Rush::Dir rather than the default Rush::File.
18
+ def self.factory(full_path, box=nil)
19
+ if full_path.tail(1) == '/'
20
+ Rush::Dir.new(full_path, box)
21
+ else
22
+ Rush::File.new(full_path, box)
23
+ end
24
+ end
25
+
26
+ def to_s # :nodoc:
27
+ if box.host == 'localhost'
28
+ "#{full_path}"
29
+ else
30
+ inspect
31
+ end
32
+ end
33
+
34
+ def inspect # :nodoc:
35
+ "#{box}:#{full_path}"
36
+ end
37
+
38
+ def connection
39
+ box ? box.connection : Rush::Connection::Local.new
40
+ end
41
+
42
+ # The parent dir. For example, box['/etc/hosts'].parent == box['etc/']
43
+ def parent
44
+ @parent ||= Rush::Dir.new(@path)
45
+ end
46
+
47
+ def full_path
48
+ "#{@path}/#{@name}"
49
+ end
50
+
51
+ # Timestamp of entry creation.
52
+ def created_at
53
+ stat[:ctime]
54
+ end
55
+
56
+ # Timestamp that entry was last modified on.
57
+ def last_modified
58
+ stat[:mtime]
59
+ end
60
+
61
+ # Timestamp that entry was last accessed on.
62
+ def last_accessed
63
+ stat[:atime]
64
+ end
65
+
66
+ # Attempts to rename, copy, or otherwise place an entry into a dir that already contains an entry by that name will fail with this exception.
67
+ class NameAlreadyExists < Exception; end
68
+
69
+ # Do not use rename or duplicate with a slash; use copy_to or move_to instead.
70
+ class NameCannotContainSlash < Exception; end
71
+
72
+ # You cannot move or copy entries to a path that is not a dir.
73
+ class NotADir < Exception; end
74
+
75
+ # Rename an entry to another name within the same dir. The object's name
76
+ # will be updated to match the change on the filesystem.
77
+ def rename(new_name)
78
+ connection.rename(@path, @name, new_name)
79
+ @name = new_name
80
+ end
81
+
82
+ # Rename an entry to another name within the same dir. The existing object
83
+ # will not be affected, but a new object representing the newly-created
84
+ # entry will be returned.
85
+ def duplicate(new_name)
86
+ raise NameCannotContainSlash if new_name.match(/\//)
87
+ new_full_path = "#{@path}/#{new_name}"
88
+ connection.copy(full_path, new_full_path)
89
+ self.class.new(new_full_path, box)
90
+ end
91
+
92
+ # Copy the entry to another dir. Returns an object representing the new
93
+ # copy.
94
+ def copy_to(dir)
95
+ raise NotADir unless dir.class == Rush::Dir
96
+
97
+ if box == dir.box
98
+ connection.copy(full_path, dir.full_path)
99
+ else
100
+ archive = connection.read_archive(full_path)
101
+ dir.box.connection.write_archive(archive, dir.full_path)
102
+ end
103
+
104
+ new_full_path = "#{dir.full_path}#{name}"
105
+ self.class.new(new_full_path, dir.box)
106
+ end
107
+
108
+ # Move the entry to another dir. The object will be updated to show its new
109
+ # location.
110
+ def move_to(dir)
111
+ moved = copy_to(dir)
112
+ destroy
113
+ mimic(moved)
114
+ end
115
+
116
+ def mimic(from) # :nodoc:
117
+ @box = from.box
118
+ @path = from.path
119
+ @name = from.name
120
+ end
121
+
122
+ # Unix convention considers entries starting with a . to be hidden.
123
+ def hidden?
124
+ name.slice(0, 1) == '.'
125
+ end
126
+
127
+ # Destroy the entry. If it is a dir, everything inside it will also be destroyed.
128
+ def destroy
129
+ connection.destroy(full_path)
130
+ end
131
+
132
+ def ==(other) # :nodoc:
133
+ full_path == other.full_path and box == other.box
134
+ end
135
+
136
+ private
137
+
138
+ def stat
139
+ connection.stat(full_path)
140
+ end
141
+ end
@@ -0,0 +1,73 @@
1
+ # Files are a subclass of Rush::Entry. Most of the file-specific operations
2
+ # relate to manipulating the file's contents, like search and replace.
3
+ class Rush::File < Rush::Entry
4
+ def dir?
5
+ false
6
+ end
7
+
8
+ # Create a blank file.
9
+ def create
10
+ write('')
11
+ self
12
+ end
13
+
14
+ # Size in bytes on disk.
15
+ def size
16
+ stat[:size]
17
+ end
18
+
19
+ # Raw contents of the file. For non-text files, you probably want to avoid
20
+ # printing this on the screen.
21
+ def contents
22
+ connection.file_contents(full_path)
23
+ end
24
+
25
+ # Write to the file, overwriting whatever was already in it.
26
+ #
27
+ # Example: file.write "hello, world\n"
28
+ def write(new_contents)
29
+ connection.write_file(full_path, new_contents)
30
+ end
31
+
32
+ # Return an array of lines from the file, similar to stdlib's File#readlines.
33
+ def lines
34
+ contents.split("\n")
35
+ end
36
+
37
+ # Search the file's for a regular expression. Returns nil if no match, or
38
+ # each of the matching lines in its entirety.
39
+ #
40
+ # Example: box['/etc/hosts'].search(/localhost/) # -> [ "127.0.0.1 localhost\n", "::1 localhost\n" ]
41
+ def search(pattern)
42
+ matching_lines = lines.select { |line| line.match(pattern) }
43
+ matching_lines.size == 0 ? nil : matching_lines
44
+ end
45
+
46
+ # Search-and-replace file contents.
47
+ #
48
+ # Example: box['/etc/hosts'].replace_contents!(/localhost/, 'local.host')
49
+ def replace_contents!(pattern, replace_with)
50
+ write contents.gsub(pattern, replace_with)
51
+ end
52
+
53
+ # Return the file's contents, or if it doesn't exist, a blank string.
54
+ def contents_or_blank
55
+ contents rescue ""
56
+ end
57
+
58
+ # Count the number of lines in the file.
59
+ def line_count
60
+ lines.size
61
+ end
62
+
63
+ # Return an array of lines, or an empty array if the file does not exist.
64
+ def lines_or_empty
65
+ lines rescue []
66
+ end
67
+
68
+ include Rush::Commands
69
+
70
+ def entries
71
+ [ self ]
72
+ end
73
+ end
@@ -0,0 +1,18 @@
1
+ # Integer extensions for file and dir sizes (returned in bytes).
2
+ #
3
+ # Example:
4
+ #
5
+ # box['/assets/'].files_flattened.select { |f| f.size > 10.mb }
6
+ class Fixnum
7
+ def kb
8
+ self * 1024
9
+ end
10
+
11
+ def mb
12
+ kb * 1024
13
+ end
14
+
15
+ def gb
16
+ mb * 1024
17
+ end
18
+ end
@@ -0,0 +1,11 @@
1
+ # Mixin for array and string for methods I wish they had.
2
+ module Rush::HeadTail
3
+ def head(n)
4
+ slice(0, n)
5
+ end
6
+
7
+ def tail(n)
8
+ n = [ n, length ].min
9
+ slice(-n, n)
10
+ end
11
+ end
@@ -0,0 +1,224 @@
1
+ require 'fileutils'
2
+ require 'yaml'
3
+
4
+ # Rush::Box uses a connection object to execute all rush commands. If the box
5
+ # is local, Rush::Connection::Local is created. The local connection is the
6
+ # heart of rush's internals. (Users of the rush shell or library need never
7
+ # access the connection object directly, so the docs herein are intended for
8
+ # developers wishing to modify rush.)
9
+ #
10
+ # The local connection has a series of methods which do the actual work of
11
+ # modifying files, getting process lists, and so on. RushServer creates a
12
+ # local connection to handle incoming requests; the translation from a raw hash
13
+ # of parameters to an executed method is handled by
14
+ # Rush::Connection::Local#receive.
15
+ class Rush::Connection::Local
16
+ # Write raw bytes to a file.
17
+ def write_file(full_path, contents)
18
+ ::File.open(full_path, 'w') do |f|
19
+ f.write contents
20
+ end
21
+ true
22
+ end
23
+
24
+ # Read raw bytes from a file.
25
+ def file_contents(full_path)
26
+ ::File.read(full_path)
27
+ end
28
+
29
+ # Destroy a file or dir.
30
+ def destroy(full_path)
31
+ raise "No." if full_path == '/'
32
+ FileUtils.rm_rf(full_path)
33
+ true
34
+ end
35
+
36
+ # Create a dir.
37
+ def create_dir(full_path)
38
+ FileUtils.mkdir_p(full_path)
39
+ true
40
+ end
41
+
42
+ class NameAlreadyExists < Exception; end
43
+ class NameCannotContainSlash < Exception; end
44
+ class NotADir < Exception; end
45
+
46
+ # Rename an entry within a dir.
47
+ def rename(path, name, new_name)
48
+ raise NameCannotContainSlash if new_name.match(/\//)
49
+ old_full_path = "#{path}/#{name}"
50
+ new_full_path = "#{path}/#{new_name}"
51
+ raise NameAlreadyExists if ::File.exists?(new_full_path)
52
+ FileUtils.mv(old_full_path, new_full_path)
53
+ true
54
+ end
55
+
56
+ # Copy ane entry from one path to another.
57
+ def copy(src, dst)
58
+ FileUtils.cp_r(src, dst)
59
+ true
60
+ end
61
+
62
+ # Create an in-memory archive (tgz) of a file or dir, which can be
63
+ # transmitted to another server for a copy or move. Note that archive
64
+ # operations have the dir name implicit in the archive.
65
+ def read_archive(full_path)
66
+ `cd #{::File.dirname(full_path)}; tar c #{::File.basename(full_path)}`
67
+ end
68
+
69
+ # Extract an in-memory archive to a dir.
70
+ def write_archive(archive, dir)
71
+ IO.popen("cd #{dir}; tar x", "w") do |p|
72
+ p.write archive
73
+ end
74
+ end
75
+
76
+ # Get an index of files from the given path with the glob. Could return
77
+ # nested values if the glob contains a doubleglob. The return value is an
78
+ # array of full paths, with directories listed first.
79
+ def index(base_path, glob)
80
+ glob = '*' if glob == '' or glob.nil?
81
+ dirs = []
82
+ files = []
83
+ ::Dir.chdir(base_path) do
84
+ ::Dir.glob(glob).each do |fname|
85
+ if ::File.directory?(fname)
86
+ dirs << fname + '/'
87
+ else
88
+ files << fname
89
+ end
90
+ end
91
+ end
92
+ dirs + files
93
+ end
94
+
95
+ # Fetch stats (size, ctime, etc) on an entry. Size will not be accurate for dirs.
96
+ def stat(full_path)
97
+ s = ::File.stat(full_path)
98
+ {
99
+ :size => s.size,
100
+ :ctime => s.ctime,
101
+ :atime => s.atime,
102
+ :mtime => s.mtime,
103
+ }
104
+ end
105
+
106
+ # Fetch the size of a dir, since a standard file stat does not include the
107
+ # size of the contents.
108
+ def size(full_path)
109
+ `du -sb #{full_path}`.match(/(\d+)/)[1].to_i
110
+ end
111
+
112
+ # Get the list of processes as an array of hashes.
113
+ def processes
114
+ if ::File.directory? "/proc"
115
+ linux_processes
116
+ else
117
+ os_x_processes
118
+ end
119
+ end
120
+
121
+ # Process list on Linux using /proc.
122
+ def linux_processes
123
+ list = []
124
+ ::Dir["/proc/*/stat"].select { |file| file =~ /\/proc\/\d+\// }.each do |file|
125
+ begin
126
+ list << read_proc_file(file)
127
+ rescue
128
+ # process died between the dir listing and accessing the file
129
+ end
130
+ end
131
+ list
132
+ end
133
+
134
+ # Read a single file in /proc and store the parsed values in a hash suitable
135
+ # for use in the Rush::Process#new.
136
+ def read_proc_file(file)
137
+ data = ::File.read(file).split(" ")
138
+ uid = ::File.stat(file).uid
139
+ pid = data[0]
140
+ command = data[1].match(/^\((.*)\)$/)[1]
141
+ cmdline = ::File.read("/proc/#{pid}/cmdline").gsub(/\0/, ' ')
142
+ utime = data[13].to_i
143
+ ktime = data[14].to_i
144
+ vss = data[22].to_i / 1024
145
+ rss = data[23].to_i * 4
146
+ time = utime + ktime
147
+
148
+ {
149
+ :pid => pid,
150
+ :uid => uid,
151
+ :command => command,
152
+ :cmdline => cmdline,
153
+ :mem => rss,
154
+ :cpu => time,
155
+ }
156
+ end
157
+
158
+ # Process list on OS X or other unixes without a /proc.
159
+ def os_x_processes
160
+ raw = os_x_raw_ps.split("\n").slice(1, 99999)
161
+ raw.map do |line|
162
+ parse_ps(line)
163
+ end
164
+ end
165
+
166
+ # ps command used to generate list of processes on non-/proc unixes.
167
+ def os_x_raw_ps
168
+ `COLUMNS=9999 ps ax -o "pid uid rss cpu command"`
169
+ end
170
+
171
+ # Parse a single line of the ps command and return the values in a hash
172
+ # suitable for use in the Rush::Process#new.
173
+ def parse_ps(line)
174
+ m = line.split(" ", 5)
175
+ params = {}
176
+ params[:pid] = m[0]
177
+ params[:uid] = m[1]
178
+ params[:rss] = m[2]
179
+ params[:cpu] = m[3]
180
+ params[:cmdline] = m[4]
181
+ params[:command] = params[:cmdline].split(" ").first
182
+ params
183
+ end
184
+
185
+ # Returns true if the specified pid is running.
186
+ def process_alive(pid)
187
+ `ps -p #{pid} | wc -l`.to_i >= 2
188
+ end
189
+
190
+ # Terminate a process, by pid.
191
+ def kill_process(pid)
192
+ ::Process.kill('TERM', pid.to_i)
193
+ end
194
+
195
+ ####################################
196
+
197
+ # Raised when the action passed in by RushServer is not known.
198
+ class UnknownAction < Exception; end
199
+
200
+ # RushServer uses this method to transform a hash (:action plus parameters
201
+ # specific to that action type) into a method call on the connection. The
202
+ # returned value must be text so that it can be transmitted across the wire
203
+ # as an HTTP response.
204
+ def receive(params)
205
+ case params[:action]
206
+ when 'write_file' then write_file(params[:full_path], params[:payload])
207
+ when 'file_contents' then file_contents(params[:full_path])
208
+ when 'destroy' then destroy(params[:full_path])
209
+ when 'create_dir' then create_dir(params[:full_path])
210
+ when 'rename' then rename(params[:path], params[:name], params[:new_name])
211
+ when 'copy' then copy(params[:src], params[:dst])
212
+ when 'read_archive' then read_archive(params[:full_path])
213
+ when 'write_archive' then write_archive(params[:payload], params[:dir])
214
+ when 'index' then index(params[:base_path], params[:glob]).join("\n") + "\n"
215
+ when 'stat' then YAML.dump(stat(params[:full_path]))
216
+ when 'size' then size(params[:full_path])
217
+ when 'processes' then YAML.dump(processes)
218
+ when 'process_alive' then process_alive(params[:pid]) ? '1' : '0'
219
+ when 'kill_process' then kill_process(params[:pid])
220
+ else
221
+ raise UnknownAction
222
+ end
223
+ end
224
+ end