all repos — flounder @ 24cf8537308c40c3b8258492496004fa039514f1

A small site builder for the Gemini protocol

utils.go (view raw)

  1package main
  2
  3import (
  4	"archive/zip"
  5	"bufio"
  6	"fmt"
  7	"io"
  8	"mime"
  9	"net"
 10	"os"
 11	"path"
 12	"path/filepath"
 13	"strings"
 14	"time"
 15	"unicode/utf8"
 16)
 17
 18func getSchemedFlounderLinkLines(r io.Reader) []string {
 19	scanner := bufio.NewScanner(r)
 20	result := []string{}
 21	for scanner.Scan() {
 22		text := scanner.Text()
 23		// TODO use actual parser
 24		if strings.HasPrefix(text, "=>") && strings.Contains(text, "."+c.Host) && (strings.Contains(text, "gemini://") || strings.Contains(text, "https://")) {
 25			result = append(result, text)
 26		}
 27	}
 28	return result
 29}
 30
 31func isOkUsername(s string) error {
 32	if len(s) < 1 {
 33		return fmt.Errorf("Username is too short")
 34	}
 35	if len(s) > 32 {
 36		return fmt.Errorf("Username is too long. 32 char max.")
 37	}
 38	for _, char := range s {
 39		if !strings.Contains(ok, strings.ToLower(string(char))) {
 40			return fmt.Errorf("Username contains invalid characters. Valid characters include lowercase letters, numbers, and hyphens.")
 41		}
 42	}
 43	for _, username := range bannedUsernames {
 44		if username == s {
 45			return fmt.Errorf("Username is not allowed.")
 46		}
 47	}
 48	return nil
 49}
 50
 51// Check if it is a text file, first by checking mimetype, then by reading bytes
 52// Stolen from https://github.com/golang/tools/blob/master/godoc/util/util.go
 53func isTextFile(fullPath string) bool {
 54	isText := strings.HasPrefix(mime.TypeByExtension(path.Ext(fullPath)), "text")
 55	if isText {
 56		return true
 57	}
 58	const max = 1024 // at least utf8.UTFMax
 59	s := make([]byte, 1024)
 60	f, err := os.Open(fullPath)
 61	if os.IsNotExist(err) {
 62		return true // for the purposes of editing, we return true
 63	}
 64	n, err := f.Read(s)
 65	s = s[0:n]
 66	if err != nil {
 67		return false
 68	}
 69	f.Close()
 70
 71	for i, c := range string(s) {
 72		if i+utf8.UTFMax > len(s) {
 73			// last char may be incomplete - ignore
 74			break
 75		}
 76		if c == 0xFFFD || c < ' ' && c != '\n' && c != '\t' && c != '\f' {
 77			// decoding error or control character - not a text file
 78			return false
 79		}
 80	}
 81	return true
 82}
 83
 84// get the user-reltaive local path from the filespath
 85// NOTE -- dont use on unsafe input ( I think )
 86func getLocalPath(filesPath string) string {
 87	l := len(strings.Split(c.FilesDirectory, "/"))
 88	return strings.Join(strings.Split(filesPath, "/")[l+1:], "/")
 89}
 90
 91func getCreator(filePath string) string {
 92	l := len(strings.Split(c.FilesDirectory, "/"))
 93	r := strings.Split(filePath, "/")[l]
 94	return r
 95}
 96
 97func isGemini(filename string) bool {
 98	extension := path.Ext(filename)
 99	return extension == ".gmi" || extension == ".gemini"
100}
101
102func timeago(t *time.Time) string {
103	d := time.Since(*t)
104	if d.Seconds() < 60 {
105		seconds := int(d.Seconds())
106		if seconds == 1 {
107			return "1 second ago"
108		}
109		return fmt.Sprintf("%d seconds ago", seconds)
110	} else if d.Minutes() < 60 {
111		minutes := int(d.Minutes())
112		if minutes == 1 {
113			return "1 minute ago"
114		}
115		return fmt.Sprintf("%d minutes ago", minutes)
116	} else if d.Hours() < 24 {
117		hours := int(d.Hours())
118		if hours == 1 {
119			return "1 hour ago"
120		}
121		return fmt.Sprintf("%d hours ago", hours)
122	} else {
123		days := int(d.Hours()) / 24
124		if days == 1 {
125			return "1 day ago"
126		}
127		return fmt.Sprintf("%d days ago", days)
128	}
129}
130func GetIPFromRemoteAddress(remoteAddress string) string {
131	ip, _, err := net.SplitHostPort(remoteAddress)
132	if err == nil {
133		return ip
134	}
135	return remoteAddress
136}
137
138// safe
139func getUserDirectory(username string) string {
140	// extra filepath.clean just to be safe
141	userFolder := path.Join(c.FilesDirectory, filepath.Clean(username))
142	return userFolder
143}
144
145// ugh idk
146func safeGetFilePath(username string, filename string) string {
147	return path.Join(getUserDirectory(username), filepath.Clean(filename))
148}
149
150func dirSize(path string) (int64, error) {
151	var size int64
152	err := filepath.Walk(path, func(_ string, info os.FileInfo, err error) error {
153		if err != nil {
154			return err
155		}
156		if !info.IsDir() {
157			size += info.Size()
158		}
159		return err
160	})
161	return size, err
162}
163
164/// Perform some checks to make sure the file is OK to upload
165func checkIfValidFile(username string, filename string, fileBytes []byte) error {
166	if len(filename) == 0 {
167		return fmt.Errorf("Please enter a filename")
168	}
169	if len(filename) > 256 { // arbitrarily chosen
170		return fmt.Errorf("Filename is too long")
171	}
172	ext := strings.ToLower(path.Ext(filename))
173	found := false
174	for _, mimetype := range c.OkExtensions {
175		if ext == mimetype {
176			found = true
177		}
178	}
179	if !found {
180		return fmt.Errorf("Invalid file extension: %s", ext)
181	}
182	if len(fileBytes) > c.MaxFileBytes {
183		return fmt.Errorf("File too large. File was %d bytes, Max file size is %d", len(fileBytes), c.MaxFileBytes)
184	}
185	userFolder := getUserDirectory(username)
186	myFiles, err := getMyFilesRecursive(userFolder, username)
187	if err != nil {
188		return err
189	}
190	if len(myFiles) >= c.MaxFilesPerUser {
191		return fmt.Errorf("You have reached the max number of files. Delete some before uploading")
192	}
193	size, err := dirSize(userFolder)
194	if err != nil || size+int64(len(fileBytes)) > c.MaxUserBytes {
195		return fmt.Errorf("You are out of storage space. Delete some files before continuing.")
196	}
197	return nil
198}
199
200func zipit(source string, target io.Writer) error {
201	archive := zip.NewWriter(target)
202
203	info, err := os.Stat(source)
204	if err != nil {
205		return nil
206	}
207
208	var baseDir string
209	if info.IsDir() {
210		baseDir = filepath.Base(source)
211	}
212
213	filepath.Walk(source, func(path string, info os.FileInfo, err error) error {
214		if err != nil {
215			return err
216		}
217
218		header, err := zip.FileInfoHeader(info)
219		if err != nil {
220			return err
221		}
222
223		if baseDir != "" {
224			header.Name = filepath.Join(baseDir, strings.TrimPrefix(path, source))
225		}
226
227		if info.IsDir() {
228			header.Name += "/"
229		} else {
230			header.Method = zip.Deflate
231		}
232
233		writer, err := archive.CreateHeader(header)
234		if err != nil {
235			return err
236		}
237
238		if info.IsDir() {
239			return nil
240		}
241
242		file, err := os.Open(path)
243		if err != nil {
244			return err
245		}
246		defer file.Close()
247		_, err = io.Copy(writer, file)
248		return err
249	})
250
251	archive.Close()
252
253	return err
254}