Skip to content

Commit 9d9cdcf

Browse files
authored
Fsutils (#98)
* 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 * Update: Implement GetFileMetadata function * Update: enhanced error handling and input validation * Update: Add test cases for better coverage * Update: Handle non-Unix systems gracefully
1 parent 5ce3479 commit 9d9cdcf

File tree

4 files changed

+276
-0
lines changed

4 files changed

+276
-0
lines changed

EXAMPLES.md

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ This document provides practical examples of how to use the library's features.
1818
14. [Time](#14-time)
1919
15. [Loggin](#15-logging)
2020
16. [File System Utilities](#16-fsutils)
21+
15. [Loggin](#15-logging)
22+
16. [File System Utilities](#16-fsutils)
2123

2224
## 1. Boolean
2325

@@ -2583,3 +2585,114 @@ func main() {
25832585
```
25842586
The directories /path/to/your/dir1 and /path/to/your/dir2 are identical.
25852587
```
2588+
2589+
### Get File Metadata
2590+
```go
2591+
package main
2592+
2593+
import (
2594+
"fmt"
2595+
"log"
2596+
2597+
"github.com/kashifkhan0771/utils/fsutils"
2598+
)
2599+
2600+
func main() {
2601+
file := "example.txt"
2602+
metadata, err := fsutils.GetFileMetadata(file)
2603+
if err != nil {
2604+
log.Fatal(err)
2605+
return
2606+
}
2607+
2608+
fmt.Printf(
2609+
"Name: %s, Size: %d, IsDir: %t, ModTime: %s, Mode: %v, Path: %s, Ext: %s, Owner: %s\n",
2610+
metadata.Name, metadata.Size,
2611+
metadata.IsDir, metadata.ModTime.String(),
2612+
metadata.Mode, metadata.Path,
2613+
metadata.Ext, metadata.Owner,
2614+
)
2615+
}
2616+
2617+
```
2618+
#### Output:
2619+
```
2620+
Name: example.txt, Size: 172, IsDir: false, ModTime: 2025-01-20 15:03:00.189199994 +0100 CET, Mode: -rw-rw-r--, Path: /path/to/your/dir/example.txt, Ext: .txt, Owner: owner
2621+
```
2622+
2623+
### Get Directory Metadata
2624+
```go
2625+
package main
2626+
2627+
import (
2628+
"fmt"
2629+
"log"
2630+
2631+
"github.com/kashifkhan0771/utils/fsutils"
2632+
)
2633+
2634+
func main() {
2635+
dir := "example/"
2636+
metadata, err := fsutils.GetFileMetadata(dir)
2637+
if err != nil {
2638+
log.Fatal(err)
2639+
return
2640+
}
2641+
2642+
fmt.Printf(
2643+
"Name: %s, Size: %d, IsDir: %t, ModTime: %s, Mode: %v, Path: %s, Ext: %s, Owner: %s\n",
2644+
metadata.Name, metadata.Size,
2645+
metadata.IsDir, metadata.ModTime.String(),
2646+
metadata.Mode, metadata.Path,
2647+
metadata.Ext, metadata.Owner,
2648+
)
2649+
}
2650+
2651+
```
2652+
#### Output:
2653+
```
2654+
Name: example, Size: 4096, IsDir: true, ModTime: 2025-01-20 15:06:23.057206656 +0100 CET, Mode: drwxrwxr-x, Path: /path/to/your/dir/example, Ext: , Owner: owner
2655+
```
2656+
2657+
### Marshal File's Metadata to JSON
2658+
```go
2659+
package main
2660+
2661+
import (
2662+
"encoding/json"
2663+
"fmt"
2664+
"log"
2665+
2666+
"github.com/kashifkhan0771/utils/fsutils"
2667+
)
2668+
2669+
func main() {
2670+
file := "example.txt"
2671+
metadata, err := fsutils.GetFileMetadata(file)
2672+
if err != nil {
2673+
log.Fatal(err)
2674+
return
2675+
}
2676+
2677+
json, err := json.Marshal(&metadata)
2678+
if err != nil {
2679+
log.Fatal(err)
2680+
return
2681+
}
2682+
2683+
fmt.Println(string(json))
2684+
}
2685+
2686+
```
2687+
#### Output:
2688+
```json
2689+
{
2690+
"name": "example.txt",
2691+
"size": 172,
2692+
"is_dir": false,
2693+
"mod_time": "2025-01-20T15:06:34.812677487+01:00",
2694+
"mode": 436,
2695+
"path": "/path/to/your/dir/example.txt",
2696+
"ext": ".txt",
2697+
}
2698+
```

fsutils/fsutils.go

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,10 @@ import (
66
"io"
77
"io/fs"
88
"os"
9+
"os/user"
910
"path/filepath"
11+
"syscall"
12+
"time"
1013
)
1114

1215
type ByteSize int64
@@ -203,3 +206,57 @@ func DirsIdentical(dir1, dir2 string) (bool, error) {
203206

204207
return len(matched) == len(files1), nil
205208
}
209+
210+
type FileMetadata struct {
211+
Name string `json:"name"`
212+
Size int64 `json:"size"`
213+
IsDir bool `json:"is_dir"`
214+
ModTime time.Time `json:"mod_time"`
215+
Mode os.FileMode `json:"mode"`
216+
Path string `json:"path"`
217+
Ext string `json:"ext"`
218+
Owner string `json:"owner"`
219+
}
220+
221+
// GetFileMetadata retrieves metadata for the specified file path.
222+
// It returns a FileMetadata struct containing details about the file.
223+
func GetFileMetadata(filePath string) (FileMetadata, error) {
224+
if filePath == "" {
225+
return FileMetadata{}, fmt.Errorf("file path cannot be empty")
226+
}
227+
228+
filePath = filepath.Clean(filePath)
229+
230+
info, err := os.Lstat(filePath)
231+
if err != nil {
232+
return FileMetadata{}, err
233+
}
234+
235+
path, err := filepath.Abs(filePath)
236+
if err != nil {
237+
return FileMetadata{}, err
238+
}
239+
240+
metadata := FileMetadata{
241+
Name: info.Name(),
242+
Size: info.Size(),
243+
IsDir: info.IsDir(),
244+
ModTime: info.ModTime(),
245+
Mode: info.Mode(),
246+
Path: path,
247+
Ext: filepath.Ext(info.Name()),
248+
}
249+
250+
if stat, ok := info.Sys().(*syscall.Stat_t); ok {
251+
owner, err := user.LookupId(fmt.Sprint(stat.Uid))
252+
if err != nil {
253+
return metadata, fmt.Errorf("failed to lookup owner: %w", err)
254+
}
255+
256+
metadata.Owner = owner.Username
257+
} else {
258+
metadata.Owner = "unknown"
259+
}
260+
261+
return metadata, nil
262+
}

fsutils/fsutils_test.go

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -396,3 +396,107 @@ func setupNestedDirs(t *testing.T) (string, string) {
396396

397397
return tempDir1, tempDir2
398398
}
399+
400+
func TestGetFileMetadata(t *testing.T) {
401+
// Create a temporary directory for testing
402+
tempDir, err := os.MkdirTemp("", "testdir")
403+
if err != nil {
404+
t.Fatal(err)
405+
}
406+
defer os.RemoveAll(tempDir)
407+
408+
// Create a test file
409+
filePath := filepath.Join(tempDir, "testfile.txt")
410+
contents := "test file contents"
411+
if err := os.WriteFile(filePath, []byte(contents), 0644); err != nil {
412+
t.Fatal(err)
413+
}
414+
415+
t.Run("Get file metadata", func(t *testing.T) {
416+
metadata, err := GetFileMetadata(filePath)
417+
if err != nil {
418+
t.Fatalf("GetFileMetadata() error = %v", err)
419+
}
420+
421+
if metadata.Name != "testfile.txt" {
422+
t.Errorf("GetFileMetadata() Name = %v; want %v", metadata.Name, "testfile.txt")
423+
}
424+
425+
if metadata.Size != int64(len(contents)) {
426+
t.Errorf("GetFileMetadata() Size = %v; want %v", metadata.Size, len(contents))
427+
}
428+
429+
if metadata.IsDir {
430+
t.Errorf("GetFileMetadata() IsDir = %v; want %v", metadata.IsDir, false)
431+
}
432+
433+
if metadata.Ext != ".txt" {
434+
t.Errorf("GetFileMetadata() Ext = %v; want %v", metadata.Ext, ".txt")
435+
}
436+
437+
if metadata.Path != filePath {
438+
t.Errorf("GetFileMetadata() Path = %v; want %v", metadata.Path, filePath)
439+
}
440+
441+
if metadata.Owner == "" {
442+
t.Errorf("GetFileMetadata() Owner should not be empty")
443+
}
444+
})
445+
446+
t.Run("Get directory metadata", func(t *testing.T) {
447+
metadata, err := GetFileMetadata(tempDir)
448+
if err != nil {
449+
t.Fatalf("GetFileMetadata() error = %v", err)
450+
}
451+
452+
if metadata.Name != filepath.Base(tempDir) {
453+
t.Errorf("GetFileMetadata() Name = %v; want %v", metadata.Name, filepath.Base(tempDir))
454+
}
455+
456+
if !metadata.IsDir {
457+
t.Errorf("GetFileMetadata() IsDir = %v; want %v", metadata.IsDir, true)
458+
}
459+
})
460+
461+
t.Run("Nonexistent file", func(t *testing.T) {
462+
_, err := GetFileMetadata(filepath.Join(tempDir, "nonexistent.txt"))
463+
if err == nil {
464+
t.Error("Expected error for nonexistent file")
465+
}
466+
})
467+
468+
t.Run("Empty path", func(t *testing.T) {
469+
_, err := GetFileMetadata("")
470+
if err == nil {
471+
t.Error("Expected error for empty path")
472+
}
473+
})
474+
475+
t.Run("Symlink", func(t *testing.T) {
476+
tempDir, err := os.MkdirTemp("", "testdir")
477+
if err != nil {
478+
t.Fatal(err)
479+
}
480+
defer os.RemoveAll(tempDir)
481+
482+
// Create a file and a symlink to it
483+
filePath := filepath.Join(tempDir, "testfile.txt")
484+
if err := os.WriteFile(filePath, []byte("test"), 0644); err != nil {
485+
t.Fatal(err)
486+
}
487+
488+
linkPath := filepath.Join(tempDir, "testlink")
489+
if err := os.Symlink(filePath, linkPath); err != nil {
490+
t.Fatal(err)
491+
}
492+
493+
metadata, err := GetFileMetadata(linkPath)
494+
if err != nil {
495+
t.Fatal(err)
496+
}
497+
498+
if metadata.Mode&os.ModeSymlink == 0 {
499+
t.Error("Expected symlink mode")
500+
}
501+
})
502+
}

readME.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -255,6 +255,8 @@ The `logging` package provides a simple, flexible, and color-coded logging syste
255255

256256
- **DirsIdentical**: Compares two directories to determine if they are identical.
257257

258+
- **GetFileMetadata**: Retrieves metadata for a specified file path. Returns a `FileMetadata` struct that can be marshaled to JSON.
259+
258260
---
259261

260262
## Examples:

0 commit comments

Comments
 (0)