利用 Golang 在 Linux 取得檔案建立時間

前一陣子利用 NFS 來將使用者上傳上來的檔案儲存在伺服器端的檔案目錄中,但是當使用者想要檢視當前已經上傳上來的檔案並且查看檔案建立的時間時,卻發現無法取得檔案建立時間。雖然可以在設計上稍做調整,例如當檔案上傳上來時,將當前時間紀錄並儲存起來,待後續檢視使用。不過這樣的方法雖然可行,但是缺點就是需要額外成本來維護所儲存的檔案建立時間。因此本篇筆者就希望利用 Linux 檔案系統所儲存的建立時間來提供檢視檔案使用。

問題

在 Golang 裡面使用 os.Stat(<file-path>).Sys().(*syscall.Stat_t) 方法可以發現結果如下:

1
&{Dev:64768 Ino:2883742 Nlink:1 Mode:33188 Uid:0 Gid:0 X__pad0:0 Rdev:0 Size:1022 Blksize:4096 Blocks:8 Atim:{Sec:1614580284 Nsec:716859675} Mtim:{Sec:1614580275 Nsec:628859301} Ctim:{Sec:1614580275 Nsec:628859301} X__unused:[0 0 0]}

由上述可以所示僅提供 Atim(Access Time), Mtim(Modify Time) 與 Ctim(Change Time),不建立時間。

另外也可以從 Linux stat 指令查看,結果如下:

1
2
3
4
5
6
7
8
  File: main.go
Size: 1022 Blocks: 8 IO Block: 4096 regular file
Device: fd00h/64768d Inode: 2883742 Links: 1
Access: (0644/-rw-r--r--) Uid: ( 0/ root) Gid: ( 0/ root)
Access: 2021-03-01 06:31:24.716859675 +0000
Modify: 2021-03-01 06:31:15.628859301 +0000
Change: 2021-03-01 06:31:15.628859301 +0000
Birth: -

由上述發現輸出結果也僅包含 Access, Modify 與 Change 三種,而 Birth 就是我們想要取得的建立時間。

解決方法

在經過筆者一番研究後發現在 Linux 4.11 版本後支援 statx() 方法,參考文件點擊。statx() 方法可以提供呼叫者檔案建立時間,但當前僅可以透過 C 語言進行呼叫,因此我們就需要透過 Golang 呼叫 C 語言,讓 C 語言代替我們去呼叫 statx() 方法,並將結果打包後轉成 Golang 看得懂的語法再將結果輸出。

接下來我們就依序介紹整個程式設計的流程。

步驟

首先我們要先確保當前 linux kernel 版本。

1
2
3
$ uname -a

Linux coredns 5.11.0-051100-generic #202102142330 SMP Sun Feb 14 23:33:21 UTC 2021 x86_64 x86_64 x86_64 GNU/Linux

當前環境已經升級到最新的 Linux Kernel 版本,若讀者尚未更新可以參考這篇文件點擊

升級完成後,就可以開始開發。首先建立一個專案,並且建立 go.mod

1
2
3
$ mkdir file-birthtime
$ cd file-birthtime
$ go mod init file-birthtime

建立 statx.h 檔案,定義方法的結構,其內容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
$ vim statx.h

#ifndef _STATX_H
#define _STATX_H

#include <asm-generic/statfs.h>

/* from kernel-sources include/uapi/linux/stat.h */
#define STATX_TYPE 0x00000001U /* Want/got stx_mode & S_IFMT */
#define STATX_MODE 0x00000002U /* Want/got stx_mode & ~S_IFMT */
#define STATX_NLINK 0x00000004U /* Want/got stx_nlink */
#define STATX_UID 0x00000008U /* Want/got stx_uid */
#define STATX_GID 0x00000010U /* Want/got stx_gid */
#define STATX_ATIME 0x00000020U /* Want/got stx_atime */
#define STATX_MTIME 0x00000040U /* Want/got stx_mtime */
#define STATX_CTIME 0x00000080U /* Want/got stx_ctime */
#define STATX_INO 0x00000100U /* Want/got stx_ino */
#define STATX_SIZE 0x00000200U /* Want/got stx_size */
#define STATX_BLOCKS 0x00000400U /* Want/got stx_blocks */
#define STATX_BASIC_STATS 0x000007ffU /* The stuff in the normal stat struct */
#define STATX_BTIME 0x00000800U /* Want/got stx_btime */
#define STATX_ALL 0x00000fffU /* All currently supported flags */
#define STATX__RESERVED 0x80000000U /* Reserved for future struct statx expansion */

struct statx_timestamp
{
__s64 tv_sec;
__u32 tv_nsec;
__s32 __reserved;
};

struct statx
{
/* 0x00 */
__u32 stx_mask; /* What results were written [uncond] */
__u32 stx_blksize; /* Preferred general I/O size [uncond] */
__u64 stx_attributes; /* Flags conveying information about the file [uncond] */
/* 0x10 */
__u32 stx_nlink; /* Number of hard links */
__u32 stx_uid; /* User ID of owner */
__u32 stx_gid; /* Group ID of owner */
__u16 stx_mode; /* File mode */
__u16 __spare0[1];
/* 0x20 */
__u64 stx_ino; /* Inode number */
__u64 stx_size; /* File size */
__u64 stx_blocks; /* Number of 512-byte blocks allocated */
__u64 stx_attributes_mask; /* Mask to show what's supported in stx_attributes */
/* 0x40 */
struct statx_timestamp stx_atime; /* Last access time */
struct statx_timestamp stx_btime; /* File creation time */
struct statx_timestamp stx_ctime; /* Last attribute change time */
struct statx_timestamp stx_mtime; /* Last data modification time */
/* 0x80 */
__u32 stx_rdev_major; /* Device ID of special file [if bdev/cdev] */
__u32 stx_rdev_minor;
__u32 stx_dev_major; /* ID of device containing file [uncond] */
__u32 stx_dev_minor;
/* 0x90 */
__u64 __spare2[14]; /* Spare space for future expansion */
/* 0x100 */
};

/* from kernel sources: include/uapi/linux/stat.h */
#define AT_STATX_SYNC_TYPE 0x6000 /* Type of synchronisation required from statx() */
#define AT_STATX_SYNC_AS_STAT 0x0000 /* - Do whatever stat() does */
#define AT_STATX_FORCE_SYNC 0x2000 /* - Force the attributes to be sync'd with the server */
#define AT_STATX_DONT_SYNC 0x4000 /* - Don't sync attributes with the server */

int date(const char *path, char *out);

#endif

上述 int date(const char *path, char *out); 方法,定義檔案路徑(path)與輸出的記憶體空間(out) 兩個參數。

建立 statx.c 檔案,定義方法的邏輯,其內容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
$ vim statx.c

#define _GNU_SOURCE
#define _ATFILE_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
#include <fcntl.h>
#include "statx.h"
#include <time.h>
#include <getopt.h>
#include <string.h>

#define VERSION 1.0.1

/*
does not (yet) provide a wrapper for the statx() system call
*/
#include <sys/syscall.h>

/* this code works ony with x86, arm64 and x86_64 */
#if __x86_64__
#define __NR_statx 332
#else
#if __aarch64__
#define __NR_statx 291
#else
#define __NR_statx 383
#endif
#endif

#define statx(a, b, c, d, e) syscall(__NR_statx, (a), (b), (c), (d), (e))

int date(const char *path, char *out)
{
struct tm tm;
time_t tim;
char buffer[100];
int len;
struct statx stxbuf;
int dirfd = AT_FDCWD;
int flags = AT_SYMLINK_NOFOLLOW;
unsigned int mask = STATX_ALL;
int n = 0;

memset(&stxbuf, 0xbf, sizeof(stxbuf));
statx(dirfd, path, flags, mask, &stxbuf);

tim = stxbuf.stx_btime.tv_sec;
if (!localtime_r(&tim, &tm))
{
perror("localtime_r");
exit(1);
}
len = strftime(buffer, 100, "%F %T", &tm);
if (len == 0)
{
perror("strftime");
exit(1);
}
n = sprintf(out, "%s.%09u", buffer, stxbuf.stx_btime.tv_nsec);
return n;
}

上述定義 date 方法的邏輯,主要呼叫 statx() 並且從輸出結構中取得 btime (Birth Time) 作為輸出。

最後,建立 main.go 利用 Golang 呼叫 C 語言定義的 date 方法取得輸出結果,並根據需求進行時間格式轉換,內容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
$ vim main.go

package main

import (
"fmt"
"os"
"time"
"unsafe"
)

// #cgo CFLAGS: -g -Wall
// #include <stdlib.h>
// #include "statx.h"
import "C"

const (
layout = "2006-01-02 15:04:05.000000000"
)

func main() {
fmt.Println(getCreatedTime(os.Args[1]))
}

func getCreatedTime(path string) (createdTime time.Time, err error) {
ppath := C.CString(path)
defer C.free(unsafe.Pointer(ppath))

date := C.malloc(C.sizeof_char * 1024)
defer C.free(unsafe.Pointer(date))

size := C.date(ppath, (*C.char)(date))
createTime := string(C.GoBytes(date, size))
t, err := time.Parse(layout, createTime)
if err != nil {
return t, err
}
return t, nil
}

結果

在執行前需先利用 go build 將 Golang 專案編譯成執行檔。

1
$ go build

編譯完成後,運行執行檔並且帶入檔案位址。

1
2
3
$ ./file-birthtime main.go

2021-03-01 06:05:36.450173642 +0000 UTC <nil>

上述可以查看當前目錄中 main.go 檔案所建立的時間。

總結

上述方法需要利用 C 語言呼叫 Linux statx() 方法取得檔案建立時間,再透過自己善用的語言與 C 語言進行溝通進而取得檔案建立時間。當然方法並非唯一,筆者希望將自己研究的方法透過文章方式跟大家分享,提供遇到相同問題的讀者一個可以解決問題的方法。

參考

若針對本篇教學有任何疑問或有敘述錯誤的地方,歡迎在底下留言討論唷~

評論

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×