all repos — flounder @ 6786b7479b3193417970da3e7365916b5f384bc7

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. this could be a little wonky
 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
142// TODO move into checkIfValidFile. rename it
143func userHasSpace(user string, newBytes int) bool {
144	userPath := path.Join(c.FilesDirectory, user)
145	size, err := dirSize(userPath)
146	if err != nil || size+int64(newBytes) > c.MaxUserBytes {
147		return false
148	}
149	return true
150}
151
152func dirSize(path string) (int64, error) {
153	var size int64
154	err := filepath.Walk(path, func(_ string, info os.FileInfo, err error) error {
155		if err != nil {
156			return err
157		}
158		if !info.IsDir() {
159			size += info.Size()
160		}
161		return err
162	})
163	return size, err
164}
165
166/// Perform some checks to make sure the file is OK
167func checkIfValidFile(filename string, fileBytes []byte) error {
168	if len(filename) == 0 {
169		return fmt.Errorf("Please enter a filename")
170	}
171	if len(filename) > 256 { // arbitrarily chosen
172		return fmt.Errorf("Filename is too long")
173	}
174	ext := strings.ToLower(path.Ext(filename))
175	found := false
176	for _, mimetype := range c.OkExtensions {
177		if ext == mimetype {
178			found = true
179		}
180	}
181	if !found {
182		return fmt.Errorf("Invalid file extension: %s", ext)
183	}
184	if len(fileBytes) > c.MaxFileBytes {
185		return fmt.Errorf("File too large. File was %d bytes, Max file size is %d", len(fileBytes), c.MaxFileBytes)
186	}
187	//
188	return nil
189}
190
191func zipit(source string, target io.Writer) error {
192	archive := zip.NewWriter(target)
193
194	info, err := os.Stat(source)
195	if err != nil {
196		return nil
197	}
198
199	var baseDir string
200	if info.IsDir() {
201		baseDir = filepath.Base(source)
202	}
203
204	filepath.Walk(source, func(path string, info os.FileInfo, err error) error {
205		if err != nil {
206			return err
207		}
208
209		header, err := zip.FileInfoHeader(info)
210		if err != nil {
211			return err
212		}
213
214		if baseDir != "" {
215			header.Name = filepath.Join(baseDir, strings.TrimPrefix(path, source))
216		}
217
218		if info.IsDir() {
219			header.Name += "/"
220		} else {
221			header.Method = zip.Deflate
222		}
223
224		writer, err := archive.CreateHeader(header)
225		if err != nil {
226			return err
227		}
228
229		if info.IsDir() {
230			return nil
231		}
232
233		file, err := os.Open(path)
234		if err != nil {
235			return err
236		}
237		defer file.Close()
238		_, err = io.Copy(writer, file)
239		return err
240	})
241
242	archive.Close()
243
244	return err
245}