290
filepathx/filepath.go
Normal file
290
filepathx/filepath.go
Normal file
@@ -0,0 +1,290 @@
|
||||
package filepathx
|
||||
|
||||
import (
|
||||
"io/fs"
|
||||
"mime"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"git.cloudyne.io/go/hiscaler-gox/filex"
|
||||
"git.cloudyne.io/go/hiscaler-gox/inx"
|
||||
)
|
||||
|
||||
const (
|
||||
searchDir = iota
|
||||
searchFile
|
||||
)
|
||||
|
||||
type WalkOption struct {
|
||||
FilterFunc func(path string) bool // 自定义函数,返回 true 则会加到列表中,否则忽略。当定义该函数时,将会忽略掉 Except, Only 设置
|
||||
Except []string // 排除的文件或者目录(仅当 FilterFunc 未设置时起作用)
|
||||
Only []string // 仅仅符合列表中的文件或者目录才会返回(仅当 FilterFunc 未设置时起作用)
|
||||
CaseSensitive bool // 区分大小写(作用于 Except 和 Only 设置)
|
||||
Recursive bool // 是否递归查询下级目录
|
||||
}
|
||||
|
||||
func read(root string, recursive bool, searchType int) []string {
|
||||
dfs := os.DirFS(root)
|
||||
paths := make([]string, 0)
|
||||
if recursive {
|
||||
fs.WalkDir(dfs, ".", func(path string, d fs.DirEntry, err error) error {
|
||||
if err == nil && path != "." && path != ".." &&
|
||||
((searchType == searchDir && d.IsDir()) || (searchType == searchFile && !d.IsDir())) {
|
||||
paths = append(paths, filepath.Join(root, path))
|
||||
}
|
||||
return nil
|
||||
})
|
||||
} else {
|
||||
ds, err := fs.ReadDir(dfs, ".")
|
||||
if err == nil {
|
||||
for _, d := range ds {
|
||||
if d.Name() != "." && d.Name() != ".." &&
|
||||
((searchType == searchDir && d.IsDir()) || (searchType == searchFile && !d.IsDir())) {
|
||||
paths = append(paths, filepath.Join(root, d.Name()))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pathPrefix := ""
|
||||
if strings.HasPrefix(root, "..") {
|
||||
pathPrefix = ".."
|
||||
} else if strings.HasPrefix(root, ".") {
|
||||
pathPrefix = "."
|
||||
}
|
||||
if pathPrefix != "" {
|
||||
pathPrefix += string(filepath.Separator)
|
||||
for i, path := range paths {
|
||||
paths[i] = pathPrefix + path
|
||||
}
|
||||
}
|
||||
return paths
|
||||
}
|
||||
|
||||
func filterPath(path string, opt WalkOption) (ok bool) {
|
||||
if (opt.FilterFunc == nil && len(opt.Only) == 0 && len(opt.Except) == 0) ||
|
||||
(opt.FilterFunc != nil && opt.FilterFunc(path)) {
|
||||
return true
|
||||
}
|
||||
|
||||
if len(opt.Except) > 0 || len(opt.Only) > 0 {
|
||||
name := filepath.Base(path)
|
||||
if len(opt.Except) > 0 {
|
||||
if opt.CaseSensitive {
|
||||
ok = true
|
||||
for _, s := range opt.Except {
|
||||
if s == name {
|
||||
ok = false
|
||||
break
|
||||
}
|
||||
}
|
||||
} else {
|
||||
ok = !inx.StringIn(name, opt.Except...)
|
||||
}
|
||||
}
|
||||
if len(opt.Only) > 0 {
|
||||
if opt.CaseSensitive {
|
||||
for _, s := range opt.Only {
|
||||
if s == name {
|
||||
ok = true
|
||||
break
|
||||
}
|
||||
}
|
||||
} else {
|
||||
ok = inx.StringIn(name, opt.Only...)
|
||||
}
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Dirs 获取指定目录下的所有目录
|
||||
func Dirs(root string, opt WalkOption) []string {
|
||||
dirs := make([]string, 0)
|
||||
paths := read(root, opt.Recursive, searchDir)
|
||||
if len(paths) > 0 {
|
||||
for _, path := range paths {
|
||||
if filterPath(path, opt) && !strings.EqualFold(path, root) {
|
||||
dirs = append(dirs, path)
|
||||
}
|
||||
}
|
||||
}
|
||||
return dirs
|
||||
}
|
||||
|
||||
// Files 获取指定目录下的所有文件
|
||||
func Files(root string, opt WalkOption) []string {
|
||||
files := make([]string, 0)
|
||||
paths := read(root, opt.Recursive, searchFile)
|
||||
if len(paths) > 0 {
|
||||
for _, path := range paths {
|
||||
if filterPath(path, opt) {
|
||||
files = append(files, path)
|
||||
}
|
||||
}
|
||||
}
|
||||
return files
|
||||
}
|
||||
|
||||
// GenerateDirNames 生成目录名
|
||||
func GenerateDirNames(s string, n, level int, caseSensitive bool) []string {
|
||||
if s == "" {
|
||||
return []string{}
|
||||
}
|
||||
|
||||
isValidCharFunc := func(r rune) bool {
|
||||
return 'A' <= r && r <= 'Z' || 'a' <= r && r <= 'z' || '0' <= r && r <= '9'
|
||||
}
|
||||
var b strings.Builder
|
||||
for _, r := range s {
|
||||
if isValidCharFunc(r) {
|
||||
b.WriteRune(r)
|
||||
}
|
||||
}
|
||||
if b.Len() == 0 {
|
||||
return []string{}
|
||||
}
|
||||
|
||||
s = b.String() // Clean s string
|
||||
if !caseSensitive {
|
||||
s = strings.ToLower(s)
|
||||
}
|
||||
if n <= 0 {
|
||||
return []string{s}
|
||||
}
|
||||
|
||||
if level <= 0 {
|
||||
level = 1
|
||||
}
|
||||
names := make([]string, 0)
|
||||
sLen := len(s)
|
||||
for i := 0; i < sLen; i += n {
|
||||
if len(names) == level {
|
||||
break
|
||||
}
|
||||
|
||||
lastIndex := i + n
|
||||
if lastIndex >= sLen {
|
||||
lastIndex = sLen
|
||||
}
|
||||
names = append(names, s[i:lastIndex])
|
||||
}
|
||||
return names
|
||||
}
|
||||
|
||||
// Ext 获取资源扩展名
|
||||
func Ext(path string, b []byte) string {
|
||||
if path == "" && b == nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
if b == nil && filex.Exists(path) {
|
||||
if b1, err := os.ReadFile(path); err == nil {
|
||||
b = b1[:512]
|
||||
}
|
||||
}
|
||||
ext := ""
|
||||
if b != nil {
|
||||
contentType := http.DetectContentType(b)
|
||||
// https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Basics_of_HTTP/MIME_types/Common_types
|
||||
extTypes := map[string][]string{
|
||||
".aac": {"audio/aac"},
|
||||
".abw": {"application/x-abiword"},
|
||||
".arc": {"application/x-freearc"},
|
||||
".avi": {"video/x-msvideo"},
|
||||
".azw": {"application/vnd.amazon.ebook"},
|
||||
// ".bin": {"application/octet-stream"},
|
||||
".bmp": {"image/bmp"},
|
||||
".bz": {"application/x-bzip"},
|
||||
".bz2": {"application/x-bzip2"},
|
||||
".csh": {"application/x-csh"},
|
||||
".css": {"text/css"},
|
||||
".csv": {"text/csv"},
|
||||
".doc": {"application/msword"},
|
||||
".docx": {"application/vnd.openxmlformats-officedocument.wordprocessingml.document"},
|
||||
".eot": {"application/vnd.ms-fontobject"},
|
||||
".epub": {"application/epub+zip"},
|
||||
".gif": {"image/gif"},
|
||||
".htm": {"text/html"},
|
||||
".html": {"text/html"},
|
||||
".ico": {"image/vnd.microsoft.icon"},
|
||||
".ics": {"text/calendar"},
|
||||
".jar": {"application/java-archive"},
|
||||
".jpg": {"image/jpeg"},
|
||||
".jpeg": {"image/jpeg"},
|
||||
".js": {"text/javascript"},
|
||||
".json": {"application/json"},
|
||||
".jsonld": {"application/ld+json"},
|
||||
".mid": {"audio/midi", "audio/x-midi"},
|
||||
".midi": {"audio/midi", "audio/x-midi"},
|
||||
".mjs": {"text/javascript"},
|
||||
".mp3": {"audio/mpeg"},
|
||||
".mpeg": {"video/mpeg"},
|
||||
".mpkg": {"application/vnd.apple.installer+xml"},
|
||||
".odp": {"application/vnd.oasis.opendocument.presentation"},
|
||||
".ods": {"application/vnd.oasis.opendocument.spreadsheet"},
|
||||
".odt": {"application/vnd.oasis.opendocument.text"},
|
||||
".oga": {"audio/ogg"},
|
||||
".ogv": {"video/ogg"},
|
||||
".ogx": {"application/ogg"},
|
||||
".otf": {"font/otf"},
|
||||
".png": {"image/png"},
|
||||
".pdf": {"application/pdf"},
|
||||
".ppt": {"application/vnd.ms-powerpoint"},
|
||||
".pptx": {"application/vnd.openxmlformats-officedocument.presentationml.presentation"},
|
||||
".rar": {"application/x-rar-compressed"},
|
||||
".rtf": {"application/rtf"},
|
||||
".sh": {"application/x-sh"},
|
||||
".svg": {"image/svg+xml"},
|
||||
".swf": {"application/x-shockwave-flash"},
|
||||
".tar": {"application/x-tar"},
|
||||
".tif": {"image/tiff"},
|
||||
".tiff": {"image/tiff"},
|
||||
".ttf": {"font/ttf"},
|
||||
".txt": {"text/plain"},
|
||||
".vsd": {"application/vnd.visio"},
|
||||
".wav": {"audio/wav"},
|
||||
".weba": {"audio/webm"},
|
||||
".webm": {"video/webm"},
|
||||
".webp": {"image/webp"},
|
||||
".woff": {"font/woff"},
|
||||
".woff2": {"font/woff2"},
|
||||
".xhtml": {"application/xhtml+xml"},
|
||||
".xls": {"application/vnd.ms-excel"},
|
||||
".xlsx": {"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"},
|
||||
".xml": {"application/xml", "text/xml"},
|
||||
".xul": {"application/vnd.mozilla.xul+xml"},
|
||||
".zip": {"application/zip"},
|
||||
".3gp": {"video/3gpp", "audio/3gpp"},
|
||||
".3g2": {"video/3gpp2", "audio/3gpp2"},
|
||||
".7z": {"application/x-7z-compressed"},
|
||||
}
|
||||
for k, types := range extTypes {
|
||||
for _, v := range types {
|
||||
mime.AddExtensionType(k, v)
|
||||
}
|
||||
}
|
||||
if extensions, err := mime.ExtensionsByType(contentType); err == nil && extensions != nil {
|
||||
n := len(extensions)
|
||||
if n == 1 {
|
||||
ext = extensions[0]
|
||||
} else {
|
||||
typeExt := map[string]string{
|
||||
"text/plain; charset=utf-8": ".txt",
|
||||
"image/jpeg": ".jpg",
|
||||
}
|
||||
if v, exists := typeExt[contentType]; exists {
|
||||
ext = v
|
||||
} else {
|
||||
ext = extensions[0]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if ext == "" {
|
||||
ext = filepath.Ext(path)
|
||||
}
|
||||
return ext
|
||||
}
|
||||
240
filepathx/filepath_test.go
Normal file
240
filepathx/filepath_test.go
Normal file
@@ -0,0 +1,240 @@
|
||||
package filepathx
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"git.cloudyne.io/go/hiscaler-gox/slicex"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestDirs(t *testing.T) {
|
||||
root, _ := os.Getwd()
|
||||
testCases := []struct {
|
||||
Number int
|
||||
Path string
|
||||
Option WalkOption
|
||||
Files []string
|
||||
}{
|
||||
{
|
||||
1,
|
||||
"/a/b",
|
||||
WalkOption{},
|
||||
[]string{},
|
||||
},
|
||||
{
|
||||
2,
|
||||
root,
|
||||
WalkOption{
|
||||
CaseSensitive: false,
|
||||
FilterFunc: func(path string) bool {
|
||||
return filepath.Base(path) == "2"
|
||||
},
|
||||
Recursive: true,
|
||||
},
|
||||
[]string{"2"},
|
||||
},
|
||||
{
|
||||
3,
|
||||
root,
|
||||
WalkOption{
|
||||
CaseSensitive: false,
|
||||
Only: []string{"2"},
|
||||
Recursive: true,
|
||||
},
|
||||
[]string{"2"},
|
||||
},
|
||||
{
|
||||
4,
|
||||
root,
|
||||
WalkOption{
|
||||
CaseSensitive: false,
|
||||
Except: []string{"2"},
|
||||
Recursive: true,
|
||||
},
|
||||
[]string{"testdata", "1", "1.1", "1.1", "1.1.1"},
|
||||
},
|
||||
{
|
||||
5,
|
||||
root,
|
||||
WalkOption{
|
||||
CaseSensitive: false,
|
||||
Recursive: true,
|
||||
},
|
||||
[]string{"testdata", "1", "1.1", "1.1", "2", "1.1.1"},
|
||||
},
|
||||
{
|
||||
6,
|
||||
root + "/testdata",
|
||||
WalkOption{
|
||||
Recursive: true,
|
||||
},
|
||||
[]string{"1", "1.1", "1.1", "2", "1.1.1"},
|
||||
},
|
||||
{
|
||||
7,
|
||||
root + "/testdata",
|
||||
WalkOption{
|
||||
Recursive: false,
|
||||
},
|
||||
[]string{"1", "2"},
|
||||
},
|
||||
}
|
||||
for _, testCase := range testCases {
|
||||
dirs := Dirs(testCase.Path, testCase.Option)
|
||||
for i, dir := range dirs {
|
||||
dirs[i] = filepath.Base(dir)
|
||||
}
|
||||
if !slicex.StringSliceEqual(dirs, testCase.Files, true, true, true) {
|
||||
t.Errorf("%d: except %v actual %v", testCase.Number, testCase.Files, dirs)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestFiles(t *testing.T) {
|
||||
root, _ := os.Getwd()
|
||||
testCases := []struct {
|
||||
Number int
|
||||
Path string
|
||||
Option WalkOption
|
||||
Files []string
|
||||
}{
|
||||
{
|
||||
1,
|
||||
"/a/b",
|
||||
WalkOption{},
|
||||
[]string{},
|
||||
},
|
||||
{
|
||||
2,
|
||||
root,
|
||||
WalkOption{
|
||||
CaseSensitive: false,
|
||||
FilterFunc: func(path string) bool {
|
||||
return filepath.Base(path) == "2.txt"
|
||||
},
|
||||
Recursive: true,
|
||||
},
|
||||
[]string{"2.txt"},
|
||||
},
|
||||
{
|
||||
3,
|
||||
root,
|
||||
WalkOption{
|
||||
CaseSensitive: false,
|
||||
Only: []string{"2.txt"},
|
||||
Recursive: true,
|
||||
},
|
||||
[]string{"2.txt"},
|
||||
},
|
||||
{
|
||||
4,
|
||||
root,
|
||||
WalkOption{
|
||||
CaseSensitive: false,
|
||||
Except: []string{"2.txt"},
|
||||
Recursive: true,
|
||||
},
|
||||
[]string{"filepath.go", "filepath_test.go", "1.1.txt", "中文_ZH (1).txt", "中文_ZH (1).txt", "中文_ZH (9).txt", "0.txt"},
|
||||
},
|
||||
{
|
||||
5,
|
||||
root,
|
||||
WalkOption{
|
||||
CaseSensitive: false,
|
||||
Recursive: true,
|
||||
},
|
||||
[]string{"filepath.go", "filepath_test.go", "1.1.txt", "2.txt", "中文_ZH (1).txt", "中文_ZH (1).txt", "中文_ZH (9).txt", "0.txt"},
|
||||
},
|
||||
{
|
||||
6,
|
||||
root + "/testdata",
|
||||
WalkOption{
|
||||
Recursive: true,
|
||||
},
|
||||
[]string{"1.1.txt", "2.txt", "中文_ZH (1).txt", "中文_ZH (1).txt", "中文_ZH (9).txt", "0.txt"},
|
||||
},
|
||||
{
|
||||
7,
|
||||
root + "/testdata",
|
||||
WalkOption{
|
||||
Recursive: false,
|
||||
},
|
||||
[]string{"0.txt"},
|
||||
},
|
||||
{
|
||||
8,
|
||||
root + "/testdata/1/1.1/1.1",
|
||||
WalkOption{
|
||||
Recursive: false,
|
||||
},
|
||||
[]string{"中文_ZH (1).txt"},
|
||||
},
|
||||
{
|
||||
9,
|
||||
"./testdata/1/1.1/1.1",
|
||||
WalkOption{
|
||||
Recursive: false,
|
||||
},
|
||||
[]string{"中文_ZH (1).txt"},
|
||||
},
|
||||
}
|
||||
for _, testCase := range testCases {
|
||||
files := Files(testCase.Path, testCase.Option)
|
||||
for i, file := range files {
|
||||
files[i] = filepath.Base(file)
|
||||
}
|
||||
if !slicex.StringSliceEqual(files, testCase.Files, true, true, true) {
|
||||
t.Errorf("%d: except %v actual %v", testCase.Number, testCase.Files, files)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenerateDirNames(t *testing.T) {
|
||||
tests := []struct {
|
||||
tag string
|
||||
string string
|
||||
n int
|
||||
level int
|
||||
caseSensitive bool
|
||||
dirs []string
|
||||
}{
|
||||
{"t1", "abc", 0, 1, true, []string{"abc"}},
|
||||
{"t2", "abc", 1, 1, true, []string{"a"}},
|
||||
{"t3", "abc", 1, 2, true, []string{"a", "b"}},
|
||||
{"t4", "abc", 1, 3, true, []string{"a", "b", "c"}},
|
||||
{"t5", "abc", 2, 1, true, []string{"ab"}},
|
||||
{"t6", "abc", 2, 2, true, []string{"ab", "c"}},
|
||||
{"t7", " a b c ", 2, 2, true, []string{"ab", "c"}},
|
||||
{"t7", " a b cdefghijklmn ", 2, 3, true, []string{"ab", "cd", "ef"}},
|
||||
{"t8", " a", 12, 3, true, []string{"a"}},
|
||||
{"t9", " a中文$b", 12, 3, true, []string{"ab"}},
|
||||
}
|
||||
for _, test := range tests {
|
||||
names := GenerateDirNames(test.string, test.n, test.level, test.caseSensitive)
|
||||
assert.Equal(t, test.dirs, names, test.tag)
|
||||
}
|
||||
}
|
||||
|
||||
func TestExt(t *testing.T) {
|
||||
root, _ := os.Getwd()
|
||||
tests := []struct {
|
||||
tag string
|
||||
path string
|
||||
b []byte
|
||||
ext string
|
||||
}{
|
||||
{"t1", "/a/b", nil, ""},
|
||||
{"t2", "https://golang.org/doc/gopher/fiveyears.jpg", nil, ".jpg"},
|
||||
{"t3", filepath.Join(root, "/testdata/2/2.txt"), nil, ".txt"},
|
||||
{"t4", filepath.Join(root, "/testdata/2/1.jpg"), nil, ".jpg"},
|
||||
{"t5", filepath.Join(root, "/testdata/2/1.pdf"), nil, ".pdf"},
|
||||
{"t6", filepath.Join(root, "/testdata/2/1111.pdf"), nil, ".pdf"},
|
||||
{"t7", filepath.Join(root, "/testdata/1.xlsx"), nil, ".xlsx"},
|
||||
}
|
||||
for _, test := range tests {
|
||||
ext := Ext(test.path, test.b)
|
||||
assert.Equal(t, test.ext, ext, test.tag)
|
||||
}
|
||||
}
|
||||
0
filepathx/testdata/0.txt
vendored
Normal file
0
filepathx/testdata/0.txt
vendored
Normal file
0
filepathx/testdata/1/1.1/1.1.1/中文_ZH (1).txt
vendored
Normal file
0
filepathx/testdata/1/1.1/1.1.1/中文_ZH (1).txt
vendored
Normal file
0
filepathx/testdata/1/1.1/1.1.1/中文_ZH (9).txt
vendored
Normal file
0
filepathx/testdata/1/1.1/1.1.1/中文_ZH (9).txt
vendored
Normal file
0
filepathx/testdata/1/1.1/1.1.txt
vendored
Normal file
0
filepathx/testdata/1/1.1/1.1.txt
vendored
Normal file
0
filepathx/testdata/1/1.1/1.1/中文_ZH (1).txt
vendored
Normal file
0
filepathx/testdata/1/1.1/1.1/中文_ZH (1).txt
vendored
Normal file
0
filepathx/testdata/2/2.txt
vendored
Normal file
0
filepathx/testdata/2/2.txt
vendored
Normal file
Reference in New Issue
Block a user