Skip to content

Commit f7125ab

Browse files
Add MAX_ROWS option for CSV rendering (#30268)
This solution implements a new config variable MAX_ROWS, which corresponds to the “Maximum allowed rows to render CSV files. (0 for no limit)” and rewrites the Render function for CSV files in markup module. Now the render function only reads the file once, having MAX_FILE_SIZE+1 as a reader limit and MAX_ROWS as a row limit. When the file is larger than MAX_FILE_SIZE or has more rows than MAX_ROWS, it only renders until the limit, and displays a user-friendly warning informing that the rendered data is not complete, in the user's language. --- Previously, when a CSV file was larger than the limit, the render function lost its function to render the code. There were also multiple reads to the file, in order to determine its size and render or pre-render. The warning: ![image](https://s3.amazonaws.com/i.snag.gy/vcKh90.jpg)
1 parent 24dace8 commit f7125ab

File tree

4 files changed

+41
-69
lines changed

4 files changed

+41
-69
lines changed

Diff for: custom/conf/app.example.ini

+3
Original file line numberDiff line numberDiff line change
@@ -1334,6 +1334,9 @@ LEVEL = Info
13341334
;;
13351335
;; Maximum allowed file size in bytes to render CSV files as table. (Set to 0 for no limit).
13361336
;MAX_FILE_SIZE = 524288
1337+
;;
1338+
;; Maximum allowed rows to render CSV files. (Set to 0 for no limit)
1339+
;MAX_ROWS = 2500
13371340

13381341
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
13391342
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

Diff for: modules/markup/csv/csv.go

+35-59
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,6 @@ package markup
55

66
import (
77
"bufio"
8-
"bytes"
9-
"fmt"
108
"html"
119
"io"
1210
"regexp"
@@ -15,6 +13,8 @@ import (
1513
"code.gitea.io/gitea/modules/csv"
1614
"code.gitea.io/gitea/modules/markup"
1715
"code.gitea.io/gitea/modules/setting"
16+
"code.gitea.io/gitea/modules/translation"
17+
"code.gitea.io/gitea/modules/util"
1818
)
1919

2020
func init() {
@@ -81,86 +81,38 @@ func writeField(w io.Writer, element, class, field string) error {
8181
func (r Renderer) Render(ctx *markup.RenderContext, input io.Reader, output io.Writer) error {
8282
tmpBlock := bufio.NewWriter(output)
8383
maxSize := setting.UI.CSV.MaxFileSize
84+
maxRows := setting.UI.CSV.MaxRows
8485

85-
if maxSize == 0 {
86-
return r.tableRender(ctx, input, tmpBlock)
86+
if maxSize != 0 {
87+
input = io.LimitReader(input, maxSize+1)
8788
}
8889

89-
rawBytes, err := io.ReadAll(io.LimitReader(input, maxSize+1))
90-
if err != nil {
91-
return err
92-
}
93-
94-
if int64(len(rawBytes)) <= maxSize {
95-
return r.tableRender(ctx, bytes.NewReader(rawBytes), tmpBlock)
96-
}
97-
return r.fallbackRender(io.MultiReader(bytes.NewReader(rawBytes), input), tmpBlock)
98-
}
99-
100-
func (Renderer) fallbackRender(input io.Reader, tmpBlock *bufio.Writer) error {
101-
_, err := tmpBlock.WriteString("<pre>")
102-
if err != nil {
103-
return err
104-
}
105-
106-
scan := bufio.NewScanner(input)
107-
scan.Split(bufio.ScanRunes)
108-
for scan.Scan() {
109-
switch scan.Text() {
110-
case `&`:
111-
_, err = tmpBlock.WriteString("&amp;")
112-
case `'`:
113-
_, err = tmpBlock.WriteString("&#39;") // "&#39;" is shorter than "&apos;" and apos was not in HTML until HTML5.
114-
case `<`:
115-
_, err = tmpBlock.WriteString("&lt;")
116-
case `>`:
117-
_, err = tmpBlock.WriteString("&gt;")
118-
case `"`:
119-
_, err = tmpBlock.WriteString("&#34;") // "&#34;" is shorter than "&quot;".
120-
default:
121-
_, err = tmpBlock.Write(scan.Bytes())
122-
}
123-
if err != nil {
124-
return err
125-
}
126-
}
127-
if err = scan.Err(); err != nil {
128-
return fmt.Errorf("fallbackRender scan: %w", err)
129-
}
130-
131-
_, err = tmpBlock.WriteString("</pre>")
132-
if err != nil {
133-
return err
134-
}
135-
return tmpBlock.Flush()
136-
}
137-
138-
func (Renderer) tableRender(ctx *markup.RenderContext, input io.Reader, tmpBlock *bufio.Writer) error {
13990
rd, err := csv.CreateReaderAndDetermineDelimiter(ctx, input)
14091
if err != nil {
14192
return err
14293
}
143-
14494
if _, err := tmpBlock.WriteString(`<table class="data-table">`); err != nil {
14595
return err
14696
}
147-
row := 1
97+
98+
row := 0
14899
for {
149100
fields, err := rd.Read()
150-
if err == io.EOF {
101+
if err == io.EOF || (row >= maxRows && maxRows != 0) {
151102
break
152103
}
153104
if err != nil {
154105
continue
155106
}
107+
156108
if _, err := tmpBlock.WriteString("<tr>"); err != nil {
157109
return err
158110
}
159111
element := "td"
160-
if row == 1 {
112+
if row == 0 {
161113
element = "th"
162114
}
163-
if err := writeField(tmpBlock, element, "line-num", strconv.Itoa(row)); err != nil {
115+
if err := writeField(tmpBlock, element, "line-num", strconv.Itoa(row+1)); err != nil {
164116
return err
165117
}
166118
for _, field := range fields {
@@ -174,8 +126,32 @@ func (Renderer) tableRender(ctx *markup.RenderContext, input io.Reader, tmpBlock
174126

175127
row++
176128
}
129+
177130
if _, err = tmpBlock.WriteString("</table>"); err != nil {
178131
return err
179132
}
133+
134+
// Check if maxRows or maxSize is reached, and if true, warn.
135+
if (row >= maxRows && maxRows != 0) || (rd.InputOffset() >= maxSize && maxSize != 0) {
136+
warn := `<table class="data-table"><tr><td>`
137+
rawLink := ` <a href="` + ctx.Links.RawLink() + `/` + util.PathEscapeSegments(ctx.RelativePath) + `">`
138+
139+
// Try to get the user translation
140+
if locale, ok := ctx.Ctx.Value(translation.ContextKey).(translation.Locale); ok {
141+
warn += locale.TrString("repo.file_too_large")
142+
rawLink += locale.TrString("repo.file_view_raw")
143+
} else {
144+
warn += "The file is too large to be shown."
145+
rawLink += "View Raw"
146+
}
147+
148+
warn += rawLink + `</a></td></tr></table>`
149+
150+
// Write the HTML string to the output
151+
if _, err := tmpBlock.WriteString(warn); err != nil {
152+
return err
153+
}
154+
}
155+
180156
return tmpBlock.Flush()
181157
}

Diff for: modules/markup/csv/csv_test.go

-10
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,6 @@
44
package markup
55

66
import (
7-
"bufio"
8-
"bytes"
97
"strings"
108
"testing"
119

@@ -31,12 +29,4 @@ func TestRenderCSV(t *testing.T) {
3129
assert.NoError(t, err)
3230
assert.EqualValues(t, v, buf.String())
3331
}
34-
35-
t.Run("fallbackRender", func(t *testing.T) {
36-
var buf bytes.Buffer
37-
err := render.fallbackRender(strings.NewReader("1,<a>\n2,<b>"), bufio.NewWriter(&buf))
38-
assert.NoError(t, err)
39-
want := "<pre>1,&lt;a&gt;\n2,&lt;b&gt;</pre>"
40-
assert.Equal(t, want, buf.String())
41-
})
4232
}

Diff for: modules/setting/ui.go

+3
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ var UI = struct {
5252

5353
CSV struct {
5454
MaxFileSize int64
55+
MaxRows int
5556
} `ini:"ui.csv"`
5657

5758
Admin struct {
@@ -107,8 +108,10 @@ var UI = struct {
107108
},
108109
CSV: struct {
109110
MaxFileSize int64
111+
MaxRows int
110112
}{
111113
MaxFileSize: 524288,
114+
MaxRows: 2500,
112115
},
113116
Admin: struct {
114117
UserPagingNum int

0 commit comments

Comments
 (0)