Better detection of binary files

Author Mitja Felicijan <mitja.felicijan@gmail.com> 2026-05-08 09:59:17 +0200
Committer Mitja Felicijan <mitja.felicijan@gmail.com> 2026-05-08 09:59:17 +0200
Commit 282941de8b2111beadb8d277af0b0dd7c0b71a96 (patch)
-rw-r--r-- hfiles.go 47
1 files changed, 45 insertions, 2 deletions
diff --git a/hfiles.go b/hfiles.go
...
10
	"net/http"
10
	"net/http"
11
	"path"
11
	"path"
12
	"sort"
12
	"sort"
  
13
	"strconv"
13
	"strings"
14
	"strings"
14
  
15
  
15
	"github.com/go-git/go-git/v5/plumbing"
16
	"github.com/go-git/go-git/v5/plumbing"
...
115
		return
116
		return
116
	}
117
	}
117
  
118
  
  
119
	isBinary, _ := file.IsBinary()
  
120
	if isBinary {
  
121
		http.Redirect(w, r, fmt.Sprintf("/r/%s/raw/%s?ref=%s", ctx.Repo.Name, pathValue, ctx.CurrentRef), http.StatusFound)
  
122
		return
  
123
	}
  
124
  
118
	content, err := file.Contents()
125
	content, err := file.Contents()
119
	if err != nil {
126
	if err != nil {
120
		http.Error(w, fmt.Sprintf("Error reading file: %v", err), http.StatusInternalServerError)
127
		http.Error(w, fmt.Sprintf("Error reading file: %v", err), http.StatusInternalServerError)
...
164
	}
171
	}
165
	defer reader.Close()
172
	defer reader.Close()
166
  
173
  
167
	w.Header().Set("Content-Type", "application/octet-stream")
174
	// Read first 512 bytes to detect content type
168
	w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", pathValue))
175
	data := make([]byte, 512)
  
176
	n, err := io.ReadFull(reader, data)
  
177
	if err != nil && err != io.EOF && err != io.ErrUnexpectedEOF {
  
178
		http.Error(w, fmt.Sprintf("Error reading file: %v", err), http.StatusInternalServerError)
  
179
		return
  
180
	}
  
181
	data = data[:n]
  
182
  
  
183
	contentType := http.DetectContentType(data)
  
184
  
  
185
	// Fallback for some common types if DetectContentType returns octet-stream or text/plain
  
186
	if contentType == "application/octet-stream" || contentType == "text/plain; charset=utf-8" {
  
187
		ext := strings.ToLower(path.Ext(pathValue))
  
188
		switch ext {
  
189
		case ".go", ".ts", ".js", ".md", ".txt", ".json", ".yaml", ".yml", ".sh", ".c", ".h", ".cpp", ".hpp", ".css", ".html":
  
190
			contentType = "text/plain; charset=utf-8"
  
191
		case ".pdf":
  
192
			contentType = "application/pdf"
  
193
		case ".svg":
  
194
			contentType = "image/svg+xml"
  
195
		}
  
196
	}
  
197
  
  
198
	w.Header().Set("Content-Type", contentType)
  
199
	w.Header().Set("Content-Length", strconv.FormatInt(file.Size, 10))
  
200
  
  
201
	// Decide if it should be inline or attachment
  
202
	disposition := "inline"
  
203
	if contentType == "application/octet-stream" {
  
204
		disposition = "attachment"
  
205
	}
  
206
  
  
207
	w.Header().Set("Content-Disposition", fmt.Sprintf("%s; filename=\"%s\"", disposition, path.Base(pathValue)))
  
208
  
  
209
	// Write the first bytes we read
  
210
	w.Write(data)
  
211
	// Then copy the rest
169
	io.Copy(w, reader)
212
	io.Copy(w, reader)
170
}
213
}
171
  
214
  
...