【sduoj】实现文件上传功能

2021SC@SDUSC

引言

在sduoj项目中,文件上传是必不可少的,上传用户头像需要提交图片,上传测试点文件需要提交压缩文件,那么我们的服务器是如何接收来自用户的上传文件请求呢?

func (svc *Service) UploadFile(fileType upload.FileType, file multipart.File,
fileHeader *multipart.FileHeader) (*FileInfo, error) {
    
    
	fileName := upload.GetFileName(fileHeader.Filename)
	uploadSavaPath := upload.GetSavaPath()
	dst := uploadSavaPath + "/" + fileName
	if !upload.CheckContainExt(fileType, fileName) {
    
    
		return nil, errors.New("file suffix is not supported")
	}
	if upload.CheckSavaPath(uploadSavaPath) {
    
    
		err := upload.CreateSavaPath(uploadSavaPath, os.ModePerm)
		if err != nil {
    
    
			return nil, errors.New("failed to create save directory")
		}
	}
	if upload.CheckMaxSize(fileType, file) {
    
    
		return nil, errors.New("exceeded maximum file limit")
	}
	if upload.CheckPermission(uploadSavaPath) {
    
    
		return nil, errors.New("insufficient file permissions")
	}
	if err := upload.SaveFile(fileHeader, dst); err != nil {
    
    
		return nil, err
	}

	accessUrl := global.AppSetting.UploadServerUrl + "/" + fileName
	return &FileInfo{
    
    Name: fileName, AccessUrl: accessUrl}, nil
}

源码分析

GetFileName

GetFileName的形参是原文件名,它将文件后缀分离出来,将不含后缀的文件名进行 MD5 加密,然后将加密后的字符串与后缀拼装起来返回。

func GetFileName(name string) string {
    
    
	ext := GetFileExt(name)
	fileName := strings.TrimSuffix(name, ext)
	fileName = util.EncodeMD5(fileName)

	return fileName + ext
}

GetFileExt来获取文件文件后缀,它调用了path.Ext方法。

func GetFileExt(name string) string {
    
    
	return path.Ext(name)
}

path.Ext方法中的for循环会对路径名的每一个字符从后往前进行遍历,直到遍历完成或遍历到上一目录,如果遇到.,说明此时找到了完整的后缀,然后将后缀返回。注意,path[i:]从第i位开始(包含.),一直到到路径末尾。

func Ext(path string) string {
    
    
	for i := len(path) - 1; i >= 0 && path[i] != '/'; i-- {
    
    
		if path[i] == '.' {
    
    
			return path[i:]
		}
	}
	return ""
}

string.TrimSuffix方法用于去掉ssuffix后缀。它先检测s的后缀是否是suffix,如果是的话,返回就将后缀截掉,如果不是的话,就将s原样返回。

func TrimSuffix(s, suffix string) string {
    
    
	if HasSuffix(s, suffix) {
    
    
		return s[:len(s)-len(suffix)]
	}
	return s
}

string.HasSuffix方法用于检测参数中的suffix是否为s的后缀。它先检测s字符串的长度是否不低于suffix的长度(这是必要条件),满足这个条件的话len(s)-len(suffix)就是一个非负数,在后面对字符串进行接取的时候不至于出现越界错误。然后截取s字符串从后往前len(suffix)长度的字符串,比较该字符串与suffix是否相同即可。

func HasSuffix(s, suffix string) bool {
    
    
	return len(s) >= len(suffix) && s[len(s)-len(suffix):] == suffix
}

EncodeMD5会对上传的文件名进行吧 MD5 加密,避免暴露原始名称。

func EncodeMD5(value string) string {
    
    
	m := md5.New()
	m.Write([]byte(value))

	return hex.EncodeToString(m.Sum(nil))
}

GetSavePath

GetSavePath从配置文件中获取文件保存地址。

func GetSavaPath() string {
    
    
	return global.AppSetting.UploadSavePath
}

CheckContainExt

CheckContainExt检查文件后缀是否包含在约定的后缀配置项中。它先获取文件名后缀,并将后缀名转为大写

扫描二维码关注公众号,回复: 13716483 查看本文章
func CheckContainExt(t FileType, name string) bool {
    
    
	ext := GetFileExt(name)
	ext = strings.ToUpper(ext)
	switch t {
    
    
	case TypeImage:
		for _, allowExt := range global.AppSetting.UploadImageAllowExts {
    
    
			if strings.ToUpper(allowExt) == ext {
    
    
				return true
			}
		}
	}

	return true
}

string.ToUpper首先对字符串进行遍历,如果里面存在非 ASCII 码的字符,那么需要进行单独处理,使得非 ASCII 码不受影响,如果字符串中已经全部是大写字母,直接将该字符串返回即可;如果其中存在小写字母,就把这些小写字母转为大写即可。

func ToUpper(s string) string {
    
    
	isASCII, hasLower := true, false
	for i := 0; i < len(s); i++ {
    
    
		c := s[i]
		if c >= utf8.RuneSelf {
    
    
			isASCII = false
			break
		}
		hasLower = hasLower || ('a' <= c && c <= 'z')
	}

	if isASCII {
    
     // optimize for ASCII-only strings.
		if !hasLower {
    
    
			return s
		}
		var b Builder
		b.Grow(len(s))
		for i := 0; i < len(s); i++ {
    
    
			c := s[i]
			if 'a' <= c && c <= 'z' {
    
    
				c -= 'a' - 'A'
			}
			b.WriteByte(c)
		}
		return b.String()
	}
	return Map(unicode.ToUpper, s)
}

使用ToUpper的目的是,有时候上传的文件的后缀可能是大写、小写、大小写混合,为了让检测时更加的方便,我们可以让它们统一成相同的格式,然后再进行匹配。

CheckSavaPath

CheckSavaPath的作用是检查保存的路径是否存在。

func CheckSavaPath(dst string) bool {
    
    
	_, err := os.Stat(dst)
	return os.IsNotExist(err)
}

os.Stat的作用是返回一个描述dst指定文件对象的信息,如果有错误的话,它会向上返回。

func Stat(name string) (FileInfo, error) {
    
    
	testlog.Stat(name)
	return statNolog(name)
}

os.IsNotExist返回一个布尔值说明该错误是否表示一个文件或目录不存在。

func IsNotExist(err error) bool {
    
    
	return underlyingErrorIs(err, ErrNotExist)
}

CreateSavaPath

CreateSavaPath会创建保存上传文件的目录。os.MkdirAll方法会以传入的os.FileMode权限位递归创建所需的所有目录结构,若设计的目录已存在,则不进行任何操作。

func CreateSavaPath(dst string, perm os.FileMode) error {
    
    
	err := os.MkdirAll(dst, perm)
	if err != nil {
    
    
		return err
	}

	return nil
}

os.MkdirAll先获取path的文件信息,如果存在这个文件,那么就查看它是否为目录文件,如果是的话,就说明目录已经创建完毕,可以直接返回,如果它不是目录文件,那么会返回错误。

此时,我们知道了path所指向的最内层目录文件不存在,那么它的父目录存在吗?我们不知道,所以我们需要去检测。检测方式是,先去掉路径末尾的路径分隔符部分,如\\/,再去掉路径末尾的非路径分隔符部分,这样的话,就把最内层的路径去掉了。然后,我们递归调用MkdirAll,并把路径剩余的部分传入。这样一直找到父目录存在为止。

然后我们调用Mkdir,它会利用系统指令将创建新的目录。递归结束时,所有的目录都会被创建。

func MkdirAll(path string, perm FileMode) error {
    
    
	// Fast path: if we can tell whether path is a directory or file, stop with success or error.
	dir, err := Stat(path)
	if err == nil {
    
    
		if dir.IsDir() {
    
    
			return nil
		}
		return &PathError{
    
    Op: "mkdir", Path: path, Err: syscall.ENOTDIR}
	}

	// Slow path: make sure parent exists and then call Mkdir for path.
	i := len(path)
	for i > 0 && IsPathSeparator(path[i-1]) {
    
     // Skip trailing path separator.
		i--
	}

	j := i
	for j > 0 && !IsPathSeparator(path[j-1]) {
    
     // Scan backward over element.
		j--
	}

	if j > 1 {
    
    
		// Create parent.
		err = MkdirAll(fixRootDirectory(path[:j-1]), perm)
		if err != nil {
    
    
			return err
		}
	}

	// Parent now exists; invoke Mkdir and use its result.
	err = Mkdir(path, perm)
	if err != nil {
    
    
		// Handle arguments like "foo/." by
		// double-checking that directory doesn't exist.
		dir, err1 := Lstat(path)
		if err1 == nil && dir.IsDir() {
    
    
			return nil
		}
		return err
	}
	return nil
}

CheckMaxSize

CheckMaxSize会检查文件大小是否超出限制,它先读取这个文件并获取到该文件的长度,将这个长度与系统允许上传的最大长度相比较,如果超出限制,返回true,如果没有超出限制,返回false

func CheckMaxSize(t FileType, f multipart.File) bool {
    
    
	content, _ := ioutil.ReadAll(f)
	size := len(content)
	switch t {
    
    
	case TypeImage:
		if size >= global.AppSetting.UploadImageMaxSize*1024*1024 {
    
    
			return true
		}
	}

	return false
}

ReadAll会从r中读取数据直到读完整个文件或者遇到错误,并把读取到的数据和错误返回。

func ReadAll(r io.Reader) ([]byte, error) {
    
    
	return io.ReadAll(r)
}

CheckPermission

CheckPermission用于检查文件的权限。它先查看文件信息,如果权限足够,返回false,如果权限不足,返回true

func CheckPermission(dst string) bool {
    
    
	_, err := os.Stat(dst)
	return os.IsPermission(err)
}

os.IsPermission返回一个布尔值说明该错误是否表示因权限不足要求被拒绝。

func IsPermission(err error) bool {
    
    
	return underlyingErrorIs(err, ErrPermission)
}

SaveFile

SaveFile保存上传的文件,该方法通过调用os.Create方法创建目标地址文件,再通过file.Open方法打开源地址的文件,结合io.Copy方法实现二者之间的文件内容拷贝。

func SaveFile(file *multipart.FileHeader, dst string) error {
    
    
	src, err := file.Open()
	if err != nil {
    
    
		return err
	}
	defer src.Close()

	out, err := os.Create(dst)
	if err != nil {
    
    
		return err
	}
	defer out.Close()

	_, err = io.Copy(out, src)
	return err
}

猜你喜欢

转载自blog.csdn.net/weixin_45922876/article/details/120798952
今日推荐