all repos — flounder @ 42903ffa42875d46c73e94371e6ccc0d3f82b307

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