16
# is loaded through an <img> tag, which browsers sandbox.
17
PREVIEW_IMAGE_TYPES = RAW_IMAGE_TYPES.merge("svg" => "image/svg+xml").freeze
18
19
+ MARKDOWN_EXTENSIONS = %w[md markdown mdown mkd].freeze
20
+
21
+ # Above this many bytes a text file is shown as plain (un-highlighted) text:
22
+ # tokenizing megabyte-scale files is the main source of render jank.
23
+ MAX_HIGHLIGHT_BYTES = 512 * 1024
24
+ # Above this many bytes we don't render Markdown — show source instead.
25
+ MAX_MARKDOWN_BYTES = 512 * 1024
26
+ # Above this we don't read the blob into memory at all; only "view raw" works.
27
+ MAX_LOAD_BYTES = 5 * 1024 * 1024
28
+ # Display cap so a huge file never renders tens of thousands of <tr>s.
29
+ MAX_DISPLAY_LINES = 5_000
30
+
31
def show
32
# A file path like ".../app.js" or "...style.css" makes Rails negotiate a
33
# non-HTML format from the trailing extension — which has no template (→
41
@branch = params[:branch]
42
@file_path = params[:path]
43
@path_parts = @file_path.to_s.split("/").reject(&:blank?)
44
+ @filename = File.basename(@file_path)
45
+ @extension = File.extname(@filename).delete_prefix(".").downcase
46
+ @plain = params[:plain].present?
47
33
- content = GitRepositoryService.file_content(@repository.disk_path, @branch, @file_path)
34
- if content.nil?
48
+ @blob_sha = GitRepositoryService.blob_sha(@repository.disk_path, @branch, @file_path)
49
+ @file_size = GitRepositoryService.blob_size(@repository.disk_path, @branch, @file_path)
50
+ if @blob_sha.nil? || @file_size.nil?
51
render file: Rails.public_path.join("404.html"), status: :not_found, layout: false
52
return
53
end
54
39
- @raw_content = content
40
- @filename = File.basename(@file_path)
41
- @extension = File.extname(@filename).delete_prefix(".")
55
@commit_count = GitRepositoryService.commit_count(@repository.disk_path, @branch)
43
- @is_binary = content.encoding != Encoding::UTF_8 || !content.valid_encoding? || binary_content?(content)
44
- @file_size = content.bytesize
56
+ # Pin "copy permalink" to the commit the ref points at right now, so the
57
+ # link keeps resolving to this exact content after the branch moves on.
58
+ @commit_sha = GitRepositoryService.commit_sha(@repository.disk_path, @branch)
59
+ @raw_path = repository_blob_raw_path(@owner.username, @repository.name, @branch, @file_path)
60
46
- @image_type = PREVIEW_IMAGE_TYPES[@extension.downcase]
61
+ @image_type = PREVIEW_IMAGE_TYPES[@extension]
62
if @image_type
48
- # Embed the image inline as a data URI so it previews without a second
49
- # request. <img>-loaded content is sandboxed, so this is safe for SVG too.
50
- @image_data_uri = "data:#{@image_type};base64,#{[ content ].pack('m0')}"
51
- elsif !@is_binary
52
- @highlighted_lines = highlight_lines(content, @filename)
53
- @line_count = content.lines.count
63
+ prepare_image
64
+ else
65
+ prepare_text
66
end
67
68
render :show
69
end
70
71
def raw
60
- @branch = params[:branch]
61
- @file_path = params[:path]
72
+ branch = params[:branch]
73
+ file_path = params[:path]
74
63
- content = GitRepositoryService.file_content(@repository.disk_path, @branch, @file_path)
75
+ content = GitRepositoryService.file_content(@repository.disk_path, branch, file_path)
76
if content.nil?
77
head :not_found
78
return
79
end
80
69
- ext = File.extname(@file_path).delete_prefix(".").downcase
81
+ ext = File.extname(file_path).delete_prefix(".").downcase
82
+ # nosniff stops the browser from re-interpreting user content (e.g. an HTML
83
+ # or SVG file served as text/plain) as active content on the app origin.
84
+ response.headers["X-Content-Type-Options"] = "nosniff"
85
send_data content,
86
type: RAW_IMAGE_TYPES[ext] || "text/plain; charset=utf-8",
87
disposition: "inline",
73
- filename: File.basename(@file_path)
88
+ filename: File.basename(file_path)
89
end
90
91
private
92
93
+ def prepare_image
94
+ if @file_size > MAX_LOAD_BYTES
95
+ @too_large = true
96
+ return
97
+ end
98
+ content = GitRepositoryService.file_content(@repository.disk_path, @branch, @file_path)
99
+ # Embed inline as a data URI so it previews without a second request.
100
+ # <img>-loaded content is sandboxed, so this is safe for SVG too.
101
+ @image_data_uri = "data:#{@image_type};base64,#{[ content ].pack('m0')}"
102
+ end
103
+
104
+ def prepare_text
105
+ if @file_size > MAX_LOAD_BYTES
106
+ @too_large = true
107
+ return
108
+ end
109
+
110
+ content = GitRepositoryService.file_content(@repository.disk_path, @branch, @file_path)
111
+ if content.nil?
112
+ render file: Rails.public_path.join("404.html"), status: :not_found, layout: false
113
+ return
114
+ end
115
+
116
+ @is_binary = content.encoding != Encoding::UTF_8 || !content.valid_encoding? || binary_content?(content)
117
+ return if @is_binary
118
+
119
+ if markdown? && !@plain && @file_size <= MAX_MARKDOWN_BYTES
120
+ @markdown_html = render_markdown(content)
121
+ return
122
+ end
123
+
124
+ @line_count = content.lines.count
125
+ lines = highlighted_lines(content)
126
+ if lines.length > MAX_DISPLAY_LINES
127
+ @truncated = true
128
+ lines = lines.first(MAX_DISPLAY_LINES)
129
+ end
130
+ @highlighted_lines = lines
131
+ end
132
+
133
+ def markdown?
134
+ MARKDOWN_EXTENSIONS.include?(@extension)
135
+ end
136
+
137
+ # Highlighted line fragments, cached by blob SHA (content-addressed, so the
138
+ # cache is shared across refs and never goes stale). Files past the highlight
139
+ # budget are escaped as plain text to keep large views snappy.
140
+ def highlighted_lines(content)
141
+ if @file_size > MAX_HIGHLIGHT_BYTES
142
+ @highlight_skipped = true
143
+ return content.lines.map { |l| ERB::Util.html_escape(l.chomp) }.map(&:html_safe)
144
+ end
145
+
146
+ Rails.cache.fetch([ "blob-hl", @blob_sha ]) do
147
+ SyntaxHighlighter.lines(content, filename: @filename)
148
+ end.map(&:html_safe)
149
+ end
150
+
151
+ def render_markdown(content)
152
+ context = MarkdownRenderer::Context.new(
153
+ username: @owner.username,
154
+ repository: @repository.name,
155
+ ref: @branch,
156
+ dir: @path_parts[0..-2].join("/")
157
+ )
158
+ Rails.cache.fetch([ "md", @blob_sha, context.ref, context.dir ]) do
159
+ MarkdownRenderer.render(content, context: context)
160
+ end.html_safe
161
+ end
162
+
163
def load_owner
164
@owner = User.find_by!(username: params[:username])
165
rescue ActiveRecord::RecordNotFound
181
def binary_content?(content)
182
content.bytes.first(8192).include?(0)
183
end
99
-
100
- def highlight_lines(content, filename)
101
- lexer = Rouge::Lexer.guess(filename: filename, source: content)
102
- html_formatter = Rouge::Formatters::HTML.new
103
-
104
- # Tokenize the entire file at once (preserves stateful lexer context across
105
- # lines — e.g. multiline strings, heredocs) then split the token stream on
106
- # newline boundaries to get one HTML fragment per source line.
107
- lines = [[]]
108
- lexer.lex(content).each do |token, value|
109
- value.split(/(\n)/, -1).each do |part|
110
- if part == "\n"
111
- lines << []
112
- else
113
- lines.last << [token, part] unless part.empty?
114
- end
115
- end
116
- end
117
-
118
- # Drop a trailing empty entry when the file ends with a newline.
119
- lines.pop if lines.last&.empty?
120
-
121
- lines.map { |line_tokens| html_formatter.format(line_tokens).html_safe }
122
- rescue StandardError
123
- content.lines.map { |l| ERB::Util.html_escape(l.chomp).html_safe }
124
- end
184
end