Class: Rails::Hyperdrive::Tools::TailLogs
- Defined in:
- lib/rails/hyperdrive/tools/tail_logs.rb
Constant Summary collapse
- DEFAULT_LINES =
200- MAX_LINES =
2_000
Class Method Summary collapse
- .call(lines: DEFAULT_LINES, file: nil, server_context: nil) ⇒ Object
-
.resolve_path(log_dir, requested) ⇒ Object
Path-traversal guard: the resolved target must stay inside log_dir.
-
.tail(path, lines) ⇒ Object
Pure-Ruby
tail -n— portable and avoids shell-escaping risk.
Methods inherited from Base
respond_error, respond_json, respond_text, with_dev_guard
Class Method Details
.call(lines: DEFAULT_LINES, file: nil, server_context: nil) ⇒ Object
20 21 22 23 24 25 26 27 28 29 30 |
# File 'lib/rails/hyperdrive/tools/tail_logs.rb', line 20 def self.call(lines: DEFAULT_LINES, file: nil, server_context: nil) with_dev_guard do n = [[lines.to_i, 1].max, MAX_LINES].min log_dir = ::Rails.root.join("log"). requested = file.to_s.empty? ? "#{::Rails.env}.log" : file.to_s log_path = resolve_path(log_dir, requested) return respond_error("log not allowed: path escapes Rails.root/log/") unless log_path return respond_error("log not found: #{log_path}") unless File.exist?(log_path) respond_text(tail(log_path, n)) end end |
.resolve_path(log_dir, requested) ⇒ Object
Path-traversal guard: the resolved target must stay inside log_dir.
33 34 35 36 37 38 39 |
# File 'lib/rails/hyperdrive/tools/tail_logs.rb', line 33 def self.resolve_path(log_dir, requested) base = Pathname.new(requested) target = base.absolute? ? base : (log_dir + base) target = Pathname.new(File.(target.to_s)) return nil unless target.to_s == log_dir.to_s || target.to_s.start_with?(log_dir.to_s + File::SEPARATOR) target end |
.tail(path, lines) ⇒ Object
Pure-Ruby tail -n — portable and avoids shell-escaping risk.
42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 |
# File 'lib/rails/hyperdrive/tools/tail_logs.rb', line 42 def self.tail(path, lines) block_size = 8 * 1024 File.open(path, "rb") do |f| f.seek(0, IO::SEEK_END) file_size = f.pos buffer = +"" newline_count = 0 while f.pos > 0 && newline_count <= lines read_size = [block_size, f.pos].min f.seek(-read_size, IO::SEEK_CUR) chunk = f.read(read_size) f.seek(-read_size, IO::SEEK_CUR) buffer.prepend(chunk) newline_count = buffer.count("\n") end buffer.force_encoding(Encoding.default_external).scrub last = buffer.lines.last(lines).join last.empty? && file_size > 0 ? buffer : last end end |