flounder

A simple gemini site builder
git clone git://git.alexwennerberg.com/flounder
Log | Files | Refs | README | LICENSE

utils.go (6204B) - raw


      1 package main
      2 
      3 import (
      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 
     18 func 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 
     31 func 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
     53 func 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 )
     86 func getLocalPath(filesPath string) string {
     87 	l := len(strings.Split(c.FilesDirectory, "/"))
     88 	return strings.Join(strings.Split(filesPath, "/")[l+1:], "/")
     89 }
     90 
     91 func getCreator(filePath string) string {
     92 	l := len(strings.Split(c.FilesDirectory, "/"))
     93 	r := strings.Split(filePath, "/")[l]
     94 	return r
     95 }
     96 
     97 func isGemini(filename string) bool {
     98 	extension := path.Ext(filename)
     99 	return extension == ".gmi" || extension == ".gemini"
    100 }
    101 
    102 const solarYearSecs = 31556926
    103 
    104 func timeago(t *time.Time) string {
    105 	d := time.Since(*t)
    106 	var metric string
    107 	var amount int
    108 	if d.Seconds() < 60 {
    109 		amount = int(d.Seconds())
    110 		metric = "second"
    111 	} else if d.Minutes() < 60 {
    112 		amount = int(d.Minutes())
    113 		metric = "minute"
    114 	} else if d.Hours() < 24 {
    115 		amount = int(d.Hours())
    116 		metric = "hour"
    117 	} else if d.Seconds() < solarYearSecs {
    118 		amount = int(d.Hours()) / 24
    119 		metric = "day"
    120 	} else {
    121 		amount = int(d.Seconds()) / solarYearSecs
    122 		metric = "year"
    123 	}
    124 	if amount == 1 {
    125 		return fmt.Sprintf("%d %s ago", amount, metric)
    126 	} else {
    127 		return fmt.Sprintf("%d %ss ago", amount, metric)
    128 	}
    129 }
    130 func 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
    139 func getUserDirectory(username string) string {
    140 	// extra filepath.clean just to be safe
    141 	userFolder := path.Join(c.FilesDirectory, cleanPath(username))
    142 	return userFolder
    143 }
    144 
    145 func safeGetFilePath(username string, filename string) string {
    146 	return path.Join(getUserDirectory(username), cleanPath(filename))
    147 }
    148 
    149 // Safe
    150 func cleanPath(thepath string) string {
    151 	res := filepath.FromSlash(path.Clean("/" + strings.Trim(thepath, "/")))
    152 	if strings.Contains(res, "..") { // sanity check
    153 		return ""
    154 	}
    155 	return res
    156 }
    157 
    158 func dirSize(path string) (int64, error) {
    159 	var size int64
    160 	err := filepath.Walk(path, func(_ string, info os.FileInfo, err error) error {
    161 		if err != nil {
    162 			return err
    163 		}
    164 		if !info.IsDir() {
    165 			size += info.Size()
    166 		}
    167 		return err
    168 	})
    169 	return size, err
    170 }
    171 
    172 /// Perform some checks to make sure the file is OK to upload
    173 func checkIfValidFile(username string, filename string, fileBytes []byte) error {
    174 	if len(filename) == 0 {
    175 		return fmt.Errorf("Please enter a filename")
    176 	}
    177 	if len(filename) > 256 { // arbitrarily chosen
    178 		return fmt.Errorf("Filename is too long")
    179 	}
    180 	ext := strings.ToLower(path.Ext(filename))
    181 	found := false
    182 	for _, mimetype := range c.OkExtensions {
    183 		if ext == mimetype {
    184 			found = true
    185 		}
    186 	}
    187 	if username == "alex" && ext == ".html" { // obviously a hack TODO
    188 		found = true
    189 	}
    190 	if !found {
    191 		return fmt.Errorf("Invalid file extension: %s", ext)
    192 	}
    193 	if len(fileBytes) > c.MaxFileBytes {
    194 		return fmt.Errorf("File too large. File was %d bytes, Max file size is %d", len(fileBytes), c.MaxFileBytes)
    195 	}
    196 	userFolder := getUserDirectory(username)
    197 	myFiles, err := getMyFilesRecursive(userFolder, username)
    198 	if err != nil {
    199 		return err
    200 	}
    201 	if len(myFiles) >= c.MaxFilesPerUser {
    202 		return fmt.Errorf("You have reached the max number of files. Delete some before uploading")
    203 	}
    204 	size, err := dirSize(userFolder)
    205 	if err != nil || size+int64(len(fileBytes)) > c.MaxUserBytes {
    206 		return fmt.Errorf("You are out of storage space. Delete some files before continuing.")
    207 	}
    208 	return nil
    209 }
    210 
    211 func zipit(source string, target io.Writer) error {
    212 	archive := zip.NewWriter(target)
    213 
    214 	info, err := os.Stat(source)
    215 	if err != nil {
    216 		return nil
    217 	}
    218 
    219 	var baseDir string
    220 	if info.IsDir() {
    221 		baseDir = filepath.Base(source)
    222 	}
    223 
    224 	filepath.Walk(source, func(path string, info os.FileInfo, err error) error {
    225 		if err != nil {
    226 			return err
    227 		}
    228 
    229 		header, err := zip.FileInfoHeader(info)
    230 		if err != nil {
    231 			return err
    232 		}
    233 
    234 		if baseDir != "" {
    235 			header.Name = filepath.Join(baseDir, strings.TrimPrefix(path, source))
    236 		}
    237 
    238 		if info.IsDir() {
    239 			header.Name += "/"
    240 		} else {
    241 			header.Method = zip.Deflate
    242 		}
    243 
    244 		writer, err := archive.CreateHeader(header)
    245 		if err != nil {
    246 			return err
    247 		}
    248 
    249 		if info.IsDir() {
    250 			return nil
    251 		}
    252 
    253 		file, err := os.Open(path)
    254 		if err != nil {
    255 			return err
    256 		}
    257 		defer file.Close()
    258 		_, err = io.Copy(writer, file)
    259 		return err
    260 	})
    261 
    262 	archive.Close()
    263 
    264 	return err
    265 }