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
方法用于去掉s
的suffix
后缀。它先检测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
检查文件后缀是否包含在约定的后缀配置项中。它先获取文件名后缀,并将后缀名转为大写
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
}