Skip to content

Commit 511f6af

Browse files
authored
File System Utilities (#90)
* Add: fsutils package with function FormatFileSize * Update: implement FindFiles function * Update: implement GetDirectorySize function * Update: implement FilesIdentical function * Update: implement DirsIdentical function * Update: Add validation for directory traversal security. * Optimize memory usage for large file comparisons. * Add symlink handling and optimize directory comparison * Update: Enhance DirsIdentical test coverage * Enhance error handling tests for FindFiles * Update: Check error return value from os.Chmod
1 parent b797e93 commit 511f6af

File tree

4 files changed

+785
-1
lines changed

4 files changed

+785
-1
lines changed

EXAMPLES.md

Lines changed: 170 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@ This document provides practical examples of how to use the library's features.
1616
12. [Math](#12-math)
1717
13. [Fake](#13-fake)
1818
14. [Time](#14-time)
19-
14. [Loggin](#15-logging)
19+
15. [Loggin](#15-logging)
20+
16. [File System Utilities](#16-fsutils)
2021

2122
## 1. Boolean
2223

@@ -2414,3 +2415,171 @@ func main() {
24142415
```
24152416
[2025-01-09 12:34:56] [INFO] CustomPrefix: This message has a custom prefix.
24162417
```
2418+
2419+
## 16. Fsutils
2420+
2421+
### Format a file size given in bytes into a human-readable format
2422+
```go
2423+
package main
2424+
2425+
import (
2426+
"fmt"
2427+
2428+
"github.com/kashifkhan0771/utils/fsutils"
2429+
)
2430+
2431+
func main() {
2432+
sizes := []int64{0, 512, 1024, 1048576, 1073741824, 1099511627776}
2433+
2434+
for _, size := range sizes {
2435+
fmt.Println(fsutils.FormatFileSize(size))
2436+
}
2437+
}
2438+
```
2439+
#### Output:
2440+
```
2441+
0 B
2442+
512 B
2443+
1.00 KB
2444+
1.00 MB
2445+
1.00 GB
2446+
1.00 TB
2447+
```
2448+
2449+
### Search for files with the specified extension
2450+
```go
2451+
package main
2452+
2453+
import (
2454+
"fmt"
2455+
"log"
2456+
2457+
"github.com/kashifkhan0771/utils/fsutils"
2458+
)
2459+
2460+
func main() {
2461+
dir := "/path/to/your/dir"
2462+
2463+
txtFiles, err := fsutils.FindFiles(dir, ".txt")
2464+
if err != nil {
2465+
log.Fatalf("Error finding .txt files: %v", err)
2466+
}
2467+
2468+
fmt.Println("TXT Files:", txtFiles)
2469+
2470+
logFiles, err := fsutils.FindFiles(dir, ".log")
2471+
if err != nil {
2472+
log.Fatalf("Error finding .log files: %v", err)
2473+
}
2474+
2475+
fmt.Println("LOG Files:", logFiles)
2476+
2477+
allFiles, err := fsutils.FindFiles(dir, "")
2478+
if err != nil {
2479+
log.Fatalf("Error finding all files: %v", err)
2480+
}
2481+
2482+
fmt.Println("All Files:", allFiles)
2483+
}
2484+
2485+
```
2486+
#### Output:
2487+
```
2488+
TXT Files: [/path/to/your/dir/file1.txt /path/to/your/dir/file2.txt /path/to/your/dir/file4.txt]
2489+
LOG Files: [/path/to/your/dir/file3.log]
2490+
All Files: [/path/to/your/dir/file1.txt /path/to/your/dir/file2.txt /path/to/your/dir/file3.log /path/to/your/dir/file4.txt]
2491+
```
2492+
2493+
### Calculate the total size (in bytes) of all files in a directory
2494+
```go
2495+
package main
2496+
2497+
import (
2498+
"fmt"
2499+
"log"
2500+
2501+
"github.com/kashifkhan0771/utils/fsutils"
2502+
)
2503+
2504+
func main() {
2505+
dir := "/path/to/your/dir"
2506+
2507+
size, err := fsutils.GetDirectorySize(dir)
2508+
if err != nil {
2509+
log.Fatalf("Error calculating directory size: %v", err)
2510+
}
2511+
2512+
fmt.Printf("The total size of directory \"%s\" is %dB\n", dir, size)
2513+
}
2514+
2515+
```
2516+
#### Output:
2517+
```
2518+
The total size of directory "/path/to/your/dir" is 6406B
2519+
```
2520+
2521+
### Compare two files
2522+
```go
2523+
package main
2524+
2525+
import (
2526+
"fmt"
2527+
"log"
2528+
2529+
"github.com/kashifkhan0771/utils/fsutils"
2530+
)
2531+
2532+
func main() {
2533+
file1 := "/path/to/your/file1.txt"
2534+
file2 := "/path/to/your/file2.txt"
2535+
2536+
identical, err := fsutils.FilesIdentical(file1, file2)
2537+
if err != nil {
2538+
log.Fatalf("Error comparing files: %v", err)
2539+
}
2540+
2541+
if identical {
2542+
fmt.Printf("The files %s and %s are identical\n", file1, file2)
2543+
} else {
2544+
fmt.Printf("The files %s and %s are not identical\n", file1, file2)
2545+
}
2546+
}
2547+
2548+
```
2549+
#### Output:
2550+
```
2551+
The files /path/to/your/file1.txt and /path/to/your/file2.txt are identical
2552+
```
2553+
2554+
### Compare two directories
2555+
```go
2556+
package main
2557+
2558+
import (
2559+
"fmt"
2560+
"log"
2561+
2562+
"github.com/kashifkhan0771/utils/fsutils"
2563+
)
2564+
2565+
func main() {
2566+
dir1 := "/path/to/your/dir1"
2567+
dir2 := "/path/to/your/dir2"
2568+
2569+
identical, err := fsutils.DirsIdentical(dir1, dir2)
2570+
if err != nil {
2571+
log.Fatalf("Error comparing directories: %v", err)
2572+
}
2573+
2574+
if identical {
2575+
fmt.Printf("The directories %s and %s are identical.\n", dir1, dir2)
2576+
} else {
2577+
fmt.Printf("The directories %s and %s are not identical.\n", dir1, dir2)
2578+
}
2579+
}
2580+
2581+
```
2582+
#### Output:
2583+
```
2584+
The directories /path/to/your/dir1 and /path/to/your/dir2 are identical.
2585+
```

fsutils/fsutils.go

Lines changed: 205 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,205 @@
1+
package fsutils
2+
3+
import (
4+
"bytes"
5+
"fmt"
6+
"io"
7+
"io/fs"
8+
"os"
9+
"path/filepath"
10+
)
11+
12+
type ByteSize int64
13+
14+
const (
15+
KB ByteSize = 1024
16+
MB ByteSize = KB * 1024
17+
GB ByteSize = MB * 1024
18+
TB ByteSize = GB * 1024
19+
)
20+
21+
// FormatFileSize formats a file size given in bytes into a human-readable string
22+
// with appropriate units (B, KB, MB, GB, TB).
23+
func FormatFileSize(size int64) string {
24+
switch {
25+
case size >= int64(TB):
26+
return fmt.Sprintf("%.2f TB", float64(size)/float64(TB))
27+
case size >= int64(GB):
28+
return fmt.Sprintf("%.2f GB", float64(size)/float64(GB))
29+
case size >= int64(MB):
30+
return fmt.Sprintf("%.2f MB", float64(size)/float64(MB))
31+
case size >= int64(KB):
32+
return fmt.Sprintf("%.2f KB", float64(size)/float64(KB))
33+
default:
34+
return fmt.Sprintf("%d B", size)
35+
}
36+
}
37+
38+
// FindFiles searches for files with the specified extension in the given root directory
39+
// and returns a slice of matching file paths.
40+
func FindFiles(root string, extension string) ([]string, error) {
41+
root = filepath.Clean(root)
42+
files := make([]string, 0)
43+
44+
err := filepath.Walk(root, func(path string, info fs.FileInfo, err error) error {
45+
if err != nil {
46+
return err
47+
}
48+
49+
if info.Mode()&os.ModeSymlink != 0 {
50+
return nil
51+
}
52+
53+
if !info.IsDir() && (filepath.Ext(path) == extension || extension == "") {
54+
files = append(files, path)
55+
}
56+
57+
return nil
58+
})
59+
60+
if err != nil {
61+
return nil, err
62+
}
63+
64+
return files, nil
65+
}
66+
67+
// GetDirectorySize calculates the total size (in bytes) of all files within the specified directory
68+
func GetDirectorySize(path string) (int64, error) {
69+
var size int64 = 0
70+
71+
err := filepath.Walk(path, func(fPath string, info fs.FileInfo, err error) error {
72+
if err != nil {
73+
return err
74+
}
75+
76+
if !info.IsDir() {
77+
size += info.Size()
78+
}
79+
80+
return nil
81+
})
82+
83+
if err != nil {
84+
return 0, err
85+
}
86+
87+
return size, nil
88+
}
89+
90+
// FilesIdentical compares two files byte by byte to determine if they are identical
91+
func FilesIdentical(file1, file2 string) (bool, error) {
92+
f1, err := os.Open(file1)
93+
if err != nil {
94+
return false, err
95+
}
96+
defer f1.Close()
97+
98+
f2, err := os.Open(file2)
99+
if err != nil {
100+
return false, err
101+
}
102+
defer f2.Close()
103+
104+
const chunkSize = 32 * 1024
105+
b1 := make([]byte, chunkSize)
106+
b2 := make([]byte, chunkSize)
107+
108+
for {
109+
n1, err1 := f1.Read(b1)
110+
n2, err2 := f2.Read(b2)
111+
112+
if n1 != n2 || !bytes.Equal(b1[:n1], b2[:n2]) {
113+
return false, nil
114+
}
115+
116+
if err1 != nil || err2 != nil {
117+
if err1 == io.EOF && err2 == io.EOF {
118+
return true, nil
119+
}
120+
121+
if err1 == io.EOF || err2 == io.EOF {
122+
return false, nil
123+
}
124+
125+
return false, fmt.Errorf("error reading files: %v, %v", err1, err2)
126+
}
127+
}
128+
}
129+
130+
// DirsIdentical compares two directories to determine if they are identical
131+
// It returns true if both directories contain the same files with identical content,
132+
// and false otherwise
133+
func DirsIdentical(dir1, dir2 string) (bool, error) {
134+
dir1 = filepath.Clean(dir1)
135+
dir2 = filepath.Clean(dir2)
136+
137+
// Check if either path is a symlink
138+
for _, dir := range []string{dir1, dir2} {
139+
info, err := os.Lstat(dir)
140+
if err != nil {
141+
return false, err
142+
}
143+
if info.Mode()&os.ModeSymlink != 0 {
144+
return false, fmt.Errorf("symlinks are not supported: %s", dir)
145+
}
146+
}
147+
148+
files1, err := FindFiles(dir1, "")
149+
if err != nil {
150+
return false, err
151+
}
152+
153+
files2, err := FindFiles(dir2, "")
154+
if err != nil {
155+
return false, err
156+
}
157+
158+
if len(files1) != len(files2) {
159+
return false, nil
160+
}
161+
162+
type result struct {
163+
path string
164+
identical bool
165+
err error
166+
}
167+
168+
workers := make(chan struct{}, 10)
169+
results := make(chan result)
170+
171+
for _, file1 := range files1 {
172+
relativePath1, err := filepath.Rel(dir1, file1)
173+
if err != nil {
174+
return false, err
175+
}
176+
177+
file2 := filepath.Join(dir2, relativePath1)
178+
if _, err := os.Lstat(file2); os.IsNotExist(err) {
179+
return false, nil
180+
}
181+
182+
workers <- struct{}{}
183+
go func(f1 string, f2 string, rPath string) {
184+
defer func() { <-workers }()
185+
identical, err := FilesIdentical(f1, f2)
186+
results <- result{f1, identical, err}
187+
}(file1, file2, relativePath1)
188+
}
189+
190+
matched := make(map[string]bool)
191+
for range files1 {
192+
r := <-results
193+
if r.err != nil {
194+
return false, r.err
195+
}
196+
197+
if !r.identical {
198+
return false, nil
199+
}
200+
201+
matched[r.path] = true
202+
}
203+
204+
return len(matched) == len(files1), nil
205+
}

0 commit comments

Comments
 (0)