mirror of
https://gitee.com/johng/gf
synced 2026-06-06 16:21:40 +08:00
去掉第三方times包
This commit is contained in:
@ -8,28 +8,23 @@
|
||||
package gfile
|
||||
|
||||
import (
|
||||
"os"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"fmt"
|
||||
"time"
|
||||
"strings"
|
||||
"bytes"
|
||||
"os/exec"
|
||||
"errors"
|
||||
"os/user"
|
||||
"runtime"
|
||||
"path/filepath"
|
||||
"fmt"
|
||||
"gitee.com/johng/gf/g/container/gtype"
|
||||
"sort"
|
||||
"gitee.com/johng/gf/g/util/gconv"
|
||||
"gitee.com/johng/gf/g/os/gfpool"
|
||||
"gitee.com/johng/gf/g/util/gregex"
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
"os/user"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// 封装了常用的文件操作方法,如需更详细的文件控制,请查看官方os包
|
||||
|
||||
|
||||
// 文件分隔符
|
||||
const (
|
||||
Separator = string(filepath.Separator)
|
||||
@ -125,85 +120,6 @@ func Info(path string) *os.FileInfo {
|
||||
return &info
|
||||
}
|
||||
|
||||
// 修改时间(秒)
|
||||
func MTime(path string) int64 {
|
||||
f, e := os.Stat(path)
|
||||
if e != nil {
|
||||
return 0
|
||||
}
|
||||
return f.ModTime().Unix()
|
||||
}
|
||||
|
||||
// 修改时间(毫秒)
|
||||
func MTimeMillisecond(path string) int64 {
|
||||
f, e := os.Stat(path)
|
||||
if e != nil {
|
||||
return 0
|
||||
}
|
||||
return int64(f.ModTime().Nanosecond()/1000000)
|
||||
}
|
||||
|
||||
// 文件大小(bytes)
|
||||
func Size(path string) int64 {
|
||||
f, e := os.Stat(path)
|
||||
if e != nil {
|
||||
return 0
|
||||
}
|
||||
return f.Size()
|
||||
}
|
||||
|
||||
// 格式化文件大小
|
||||
func ReadableSize(path string) string {
|
||||
return FormatSize(float64(Size(path)))
|
||||
}
|
||||
|
||||
// 格式化文件大小
|
||||
func FormatSize(raw float64) string {
|
||||
var t float64 = 1024
|
||||
var d float64 = 1
|
||||
|
||||
if raw < t {
|
||||
return fmt.Sprintf("%.2fB", raw/d)
|
||||
}
|
||||
|
||||
d *= 1024
|
||||
t *= 1024
|
||||
|
||||
if raw < t {
|
||||
return fmt.Sprintf("%.2fK", raw/d)
|
||||
}
|
||||
|
||||
d *= 1024
|
||||
t *= 1024
|
||||
|
||||
if raw < t {
|
||||
return fmt.Sprintf("%.2fM", raw/d)
|
||||
}
|
||||
|
||||
d *= 1024
|
||||
t *= 1024
|
||||
|
||||
if raw < t {
|
||||
return fmt.Sprintf("%.2fG", raw/d)
|
||||
}
|
||||
|
||||
d *= 1024
|
||||
t *= 1024
|
||||
|
||||
if raw < t {
|
||||
return fmt.Sprintf("%.2fT", raw/d)
|
||||
}
|
||||
|
||||
d *= 1024
|
||||
t *= 1024
|
||||
|
||||
if raw < t {
|
||||
return fmt.Sprintf("%.2fP", raw/d)
|
||||
}
|
||||
|
||||
return "TooLarge"
|
||||
}
|
||||
|
||||
// 文件移动/重命名
|
||||
func Move(src string, dst string) error {
|
||||
return os.Rename(src, dst)
|
||||
@ -273,7 +189,7 @@ func Remove(path string) error {
|
||||
return os.RemoveAll(path)
|
||||
}
|
||||
|
||||
// 文件是否可
|
||||
// 文件是否可读
|
||||
func IsReadable(path string) bool {
|
||||
result := true
|
||||
file, err := os.OpenFile(path, os.O_RDONLY, 0666)
|
||||
@ -368,68 +284,6 @@ func RealPath(path string) string {
|
||||
return p
|
||||
}
|
||||
|
||||
// (文本)读取文件内容
|
||||
func GetContents(path string) string {
|
||||
return string(GetBinContents(path))
|
||||
}
|
||||
|
||||
// (二进制)读取文件内容
|
||||
func GetBinContents(path string) []byte {
|
||||
data, err := ioutil.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
return data
|
||||
}
|
||||
|
||||
// 写入文件内容
|
||||
func putContents(path string, data []byte, flag int, perm os.FileMode) error {
|
||||
// 支持目录递归创建
|
||||
dir := Dir(path)
|
||||
if !Exists(dir) {
|
||||
if err := Mkdir(dir); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
// 创建/打开文件,使用文件指针池,默认60秒
|
||||
f, err := gfpool.OpenFile(path, flag, perm, 60000)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
if n, err := f.Write(data); err != nil {
|
||||
return err
|
||||
} else if n < len(data) {
|
||||
return io.ErrShortWrite
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Truncate
|
||||
func Truncate(path string, size int) error {
|
||||
return os.Truncate(path, int64(size))
|
||||
}
|
||||
|
||||
// (文本)写入文件内容
|
||||
func PutContents(path string, content string) error {
|
||||
return putContents(path, []byte(content), os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0666)
|
||||
}
|
||||
|
||||
// (文本)追加内容到文件末尾
|
||||
func PutContentsAppend(path string, content string) error {
|
||||
return putContents(path, []byte(content), os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0666)
|
||||
}
|
||||
|
||||
// (二进制)写入文件内容
|
||||
func PutBinContents(path string, content []byte) error {
|
||||
return putContents(path, content, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0666)
|
||||
}
|
||||
|
||||
// (二进制)追加内容到文件末尾
|
||||
func PutBinContentsAppend(path string, content []byte) error {
|
||||
return putContents(path, content, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0666)
|
||||
}
|
||||
|
||||
|
||||
// 获取当前执行文件的绝对路径
|
||||
func SelfPath() string {
|
||||
@ -502,33 +356,6 @@ func homeWindows() (string, error) {
|
||||
return home, nil
|
||||
}
|
||||
|
||||
// 获得文件内容下一个指定字节的位置
|
||||
func GetNextCharOffset(file *os.File, char string, start int64) int64 {
|
||||
c := []byte(char)[0]
|
||||
b := make([]byte, 1)
|
||||
o := start
|
||||
for {
|
||||
_, err := file.ReadAt(b, o)
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
if b[0] == c {
|
||||
return o
|
||||
}
|
||||
o++
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// 获得文件内容中两个offset之间的内容 [start, end)
|
||||
func GetBinContentByTwoOffsets(file *os.File, start int64, end int64) []byte {
|
||||
buffer := make([]byte, end - start)
|
||||
if _, err := file.ReadAt(buffer, start); err != nil {
|
||||
return nil
|
||||
}
|
||||
return buffer
|
||||
}
|
||||
|
||||
// 获取入口函数文件所在目录(main包文件目录),
|
||||
// **仅对源码开发环境有效(即仅对生成该可执行文件的系统下有效)**
|
||||
func MainPkgPath() string {
|
||||
|
||||
103
g/os/gfile/gfile_contents.go
Normal file
103
g/os/gfile/gfile_contents.go
Normal file
@ -0,0 +1,103 @@
|
||||
// Copyright 2017-2018 gf Author(https://gitee.com/johng/gf). All Rights Reserved.
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the MIT License.
|
||||
// If a copy of the MIT was not distributed with this file,
|
||||
// You can obtain one at https://gitee.com/johng/gf.
|
||||
|
||||
package gfile
|
||||
|
||||
import (
|
||||
"gitee.com/johng/gf/g/os/gfpool"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
)
|
||||
|
||||
// (文本)读取文件内容
|
||||
func GetContents(path string) string {
|
||||
return string(GetBinContents(path))
|
||||
}
|
||||
|
||||
// (二进制)读取文件内容
|
||||
func GetBinContents(path string) []byte {
|
||||
data, err := ioutil.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
return data
|
||||
}
|
||||
|
||||
// 写入文件内容
|
||||
func putContents(path string, data []byte, flag int, perm os.FileMode) error {
|
||||
// 支持目录递归创建
|
||||
dir := Dir(path)
|
||||
if !Exists(dir) {
|
||||
if err := Mkdir(dir); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
// 创建/打开文件,使用文件指针池,默认60秒
|
||||
f, err := gfpool.OpenFile(path, flag, perm, 60000)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
if n, err := f.Write(data); err != nil {
|
||||
return err
|
||||
} else if n < len(data) {
|
||||
return io.ErrShortWrite
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Truncate
|
||||
func Truncate(path string, size int) error {
|
||||
return os.Truncate(path, int64(size))
|
||||
}
|
||||
|
||||
// (文本)写入文件内容
|
||||
func PutContents(path string, content string) error {
|
||||
return putContents(path, []byte(content), os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0666)
|
||||
}
|
||||
|
||||
// (文本)追加内容到文件末尾
|
||||
func PutContentsAppend(path string, content string) error {
|
||||
return putContents(path, []byte(content), os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0666)
|
||||
}
|
||||
|
||||
// (二进制)写入文件内容
|
||||
func PutBinContents(path string, content []byte) error {
|
||||
return putContents(path, content, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0666)
|
||||
}
|
||||
|
||||
// (二进制)追加内容到文件末尾
|
||||
func PutBinContentsAppend(path string, content []byte) error {
|
||||
return putContents(path, content, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0666)
|
||||
}
|
||||
|
||||
// 获得文件内容下一个指定字节的位置
|
||||
func GetNextCharOffset(file *os.File, char string, start int64) int64 {
|
||||
c := []byte(char)[0]
|
||||
b := make([]byte, 1)
|
||||
o := start
|
||||
for {
|
||||
_, err := file.ReadAt(b, o)
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
if b[0] == c {
|
||||
return o
|
||||
}
|
||||
o++
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// 获得文件内容中两个offset之间的内容 [start, end)
|
||||
func GetBinContentByTwoOffsets(file *os.File, start int64, end int64) []byte {
|
||||
buffer := make([]byte, end - start)
|
||||
if _, err := file.ReadAt(buffer, start); err != nil {
|
||||
return nil
|
||||
}
|
||||
return buffer
|
||||
}
|
||||
73
g/os/gfile/gfile_size.go
Normal file
73
g/os/gfile/gfile_size.go
Normal file
@ -0,0 +1,73 @@
|
||||
// Copyright 2017-2018 gf Author(https://gitee.com/johng/gf). All Rights Reserved.
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the MIT License.
|
||||
// If a copy of the MIT was not distributed with this file,
|
||||
// You can obtain one at https://gitee.com/johng/gf.
|
||||
|
||||
package gfile
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
)
|
||||
|
||||
// 文件大小(bytes)
|
||||
func Size(path string) int64 {
|
||||
f, e := os.Stat(path)
|
||||
if e != nil {
|
||||
return 0
|
||||
}
|
||||
return f.Size()
|
||||
}
|
||||
|
||||
// 格式化文件大小
|
||||
func ReadableSize(path string) string {
|
||||
return FormatSize(float64(Size(path)))
|
||||
}
|
||||
|
||||
// 格式化文件大小
|
||||
func FormatSize(raw float64) string {
|
||||
var t float64 = 1024
|
||||
var d float64 = 1
|
||||
|
||||
if raw < t {
|
||||
return fmt.Sprintf("%.2fB", raw/d)
|
||||
}
|
||||
|
||||
d *= 1024
|
||||
t *= 1024
|
||||
|
||||
if raw < t {
|
||||
return fmt.Sprintf("%.2fK", raw/d)
|
||||
}
|
||||
|
||||
d *= 1024
|
||||
t *= 1024
|
||||
|
||||
if raw < t {
|
||||
return fmt.Sprintf("%.2fM", raw/d)
|
||||
}
|
||||
|
||||
d *= 1024
|
||||
t *= 1024
|
||||
|
||||
if raw < t {
|
||||
return fmt.Sprintf("%.2fG", raw/d)
|
||||
}
|
||||
|
||||
d *= 1024
|
||||
t *= 1024
|
||||
|
||||
if raw < t {
|
||||
return fmt.Sprintf("%.2fT", raw/d)
|
||||
}
|
||||
|
||||
d *= 1024
|
||||
t *= 1024
|
||||
|
||||
if raw < t {
|
||||
return fmt.Sprintf("%.2fP", raw/d)
|
||||
}
|
||||
|
||||
return "TooLarge"
|
||||
}
|
||||
29
g/os/gfile/gfile_time.go
Normal file
29
g/os/gfile/gfile_time.go
Normal file
@ -0,0 +1,29 @@
|
||||
// Copyright 2017-2018 gf Author(https://gitee.com/johng/gf). All Rights Reserved.
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the MIT License.
|
||||
// If a copy of the MIT was not distributed with this file,
|
||||
// You can obtain one at https://gitee.com/johng/gf.
|
||||
|
||||
package gfile
|
||||
|
||||
import (
|
||||
"os"
|
||||
)
|
||||
|
||||
// 文件修改时间(时间戳,秒)
|
||||
func MTime(path string) int64 {
|
||||
s, e := os.Stat(path)
|
||||
if e != nil {
|
||||
return 0
|
||||
}
|
||||
return s.ModTime().Unix()
|
||||
}
|
||||
|
||||
// 文件修改时间(时间戳,毫秒)
|
||||
func MTimeMillisecond(path string) int64 {
|
||||
s, e := os.Stat(path)
|
||||
if e != nil {
|
||||
return 0
|
||||
}
|
||||
return int64(s.ModTime().Nanosecond()/1000000)
|
||||
}
|
||||
@ -3,12 +3,9 @@ package main
|
||||
import (
|
||||
"fmt"
|
||||
"gitee.com/johng/gf/g/os/gfile"
|
||||
"gitee.com/johng/gf/g/os/gtime"
|
||||
)
|
||||
|
||||
func init() {
|
||||
fmt.Println(gfile.GoRootOfBuild())
|
||||
}
|
||||
|
||||
func main() {
|
||||
|
||||
fmt.Println(gtime.NewFromTimeStamp(gfile.MTime("/home/john/Documents/temp")).String())
|
||||
}
|
||||
|
||||
@ -1,22 +0,0 @@
|
||||
language: go
|
||||
go:
|
||||
- 1.7
|
||||
before_install:
|
||||
- go get -u github.com/golang/lint/golint
|
||||
- go get github.com/axw/gocov/gocov
|
||||
- go get github.com/mattn/goveralls
|
||||
- if ! go get code.google.com/p/go.tools/cmd/cover; then go get golang.org/x/tools/cmd/cover;
|
||||
fi
|
||||
script:
|
||||
- '[ "${TRAVIS_PULL_REQUEST}" != "false" ] || $HOME/gopath/bin/goveralls -service=travis-ci
|
||||
-repotoken $COVERALLS_TOKEN'
|
||||
- "$HOME/gopath/bin/golint ./..."
|
||||
- go vet
|
||||
- go test -bench=.* -v ./...
|
||||
notifications:
|
||||
email:
|
||||
on_success: never
|
||||
on_failure: change
|
||||
env:
|
||||
global:
|
||||
secure: JLdvT4dl+oifYzbQN5I0G8mZi1KVj3D3zTC8N3D9eLEvHY26QggS2I6M5CNAYkbGSB2RDp7gZLHui8zVzvudD5GuxP2xZe+WN4+n9lf+jLnCuB77ZjKCuDa1+Wg/o466L4PK0BaiTQahbk9qsDNOdfTzXXLEnqBDo1WtvLJ1mn02pL5Wyt3aAA4yeK3z7eUYFNxyi/KJ22bwhjIv6Qa9RHT7ZfR6x9YQSGeLZIWPUx5S+vmySd7sM7gRTw+lRmR/i0PNMHjlOKoyedAT8YLAWX28xgPtc7E5j+pBm6ZmDcN79tTelhiAS9uifEKkdsugZZjfnLBl9gks19bmXP4mxUIo1iKQgIbKVs7R/QMdH7RXW+isfQP/vtk6SNC86os+r7tSJ1EOtF1cZfQ7H4o2RevA3VVTVZ+45gnrK8v00eckbLmsGGho7RGORIt+JGvD0oe84dXdk/lFtBTzWhOJn2Ujlm6L3z3r/YMN2CGFfH5JGpsUKJh1uRzJ5MPqkdxRwcGrtN4DGC1L8tRMfYPo06dQxa8p4TnbYQSZ6qwuVZMMcNvr9c/edwLizMh9KXivPodmHpLAMAUPSyWLtF8pp305DUhajpRsUEvTvs082KJAK4bUNE+AxRIBwtuB0Z2r0DpgfVwAUIXh42l9RDxEA1mwUyX2EKolyRqxQGPv9NQ=
|
||||
@ -1,22 +0,0 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015 Dustin H
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
@ -1,64 +0,0 @@
|
||||
times
|
||||
==========
|
||||
|
||||
[](https://godoc.org/github.com/djherbis/times)
|
||||
[](https://github.com/djherbis/times/releases/latest)
|
||||
[](LICENSE.txt)
|
||||
[](https://travis-ci.org/djherbis/times)
|
||||
[](https://coveralls.io/r/djherbis/times?branch=master)
|
||||
[](https://goreportcard.com/report/github.com/djherbis/times)
|
||||
[](https://sourcegraph.com/github.com/djherbis/times?badge)
|
||||
|
||||
Usage
|
||||
------------
|
||||
File Times for #golang
|
||||
|
||||
Go has a hidden time functions for most platforms, this repo makes them accessible.
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
|
||||
"gitee.com/johng/gf/third/github.com/djherbis/times"
|
||||
)
|
||||
|
||||
func main() {
|
||||
t, err := times.Stat("myfile")
|
||||
if err != nil {
|
||||
log.Fatal(err.Error())
|
||||
}
|
||||
|
||||
log.Println(t.AccessTime())
|
||||
log.Println(t.ModTime())
|
||||
|
||||
if t.HasChangeTime() {
|
||||
log.Println(t.ChangeTime())
|
||||
}
|
||||
|
||||
if t.HasBirthTime() {
|
||||
log.Println(t.BirthTime())
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Supported Times
|
||||
------------
|
||||
| | windows | linux | solaris | dragonfly | nacl | freebsd | darwin | netbsd | openbsd | plan9 |
|
||||
|:-----:|:-------:|:-----:|:-------:|:---------:|:------:|:-------:|:----:|:------:|:-------:|:-----:|
|
||||
| atime | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
|
||||
| mtime | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
|
||||
| ctime | ✓* | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | |
|
||||
| btime | ✓ | | | | | ✓ | ✓| ✓ |
|
||||
|
||||
* Windows XP does not have ChangeTime so HasChangeTime = false,
|
||||
however Vista onward does have ChangeTime so Timespec.HasChangeTime() will
|
||||
only return false on those platforms when the syscall used to obtain them fails.
|
||||
* Also note, Get(FileInfo) will now only return values available in FileInfo.Sys(), this means Stat() is required to get ChangeTime on Windows
|
||||
|
||||
Installation
|
||||
------------
|
||||
```sh
|
||||
go get github.com/djherbis/times
|
||||
```
|
||||
@ -1,29 +0,0 @@
|
||||
package times
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func BenchmarkGet(t *testing.B) {
|
||||
fileTest(t, func(f *os.File) {
|
||||
fi, err := os.Stat(f.Name())
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
for i := 0; i < t.N; i++ {
|
||||
Get(fi)
|
||||
}
|
||||
})
|
||||
t.ReportAllocs()
|
||||
}
|
||||
|
||||
func BenchmarkStat(t *testing.B) {
|
||||
fileTest(t, func(f *os.File) {
|
||||
for i := 0; i < t.N; i++ {
|
||||
Stat(f.Name())
|
||||
}
|
||||
})
|
||||
t.ReportAllocs()
|
||||
}
|
||||
@ -1,102 +0,0 @@
|
||||
package times
|
||||
|
||||
import (
|
||||
"os"
|
||||
"syscall"
|
||||
"time"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
type timespecEx struct {
|
||||
atime
|
||||
mtime
|
||||
ctime
|
||||
btime
|
||||
}
|
||||
|
||||
// StatFile finds a Windows Timespec with ChangeTime.
|
||||
func StatFile(file *os.File) (Timespec, error) {
|
||||
return statFile(syscall.Handle(file.Fd()))
|
||||
}
|
||||
|
||||
func statFile(h syscall.Handle) (Timespec, error) {
|
||||
var fileInfo fileBasicInfo
|
||||
if err := getFileInformationByHandleEx(h, &fileInfo); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var t timespecEx
|
||||
t.atime.v = time.Unix(0, fileInfo.LastAccessTime.Nanoseconds())
|
||||
t.mtime.v = time.Unix(0, fileInfo.LastWriteTime.Nanoseconds())
|
||||
t.ctime.v = time.Unix(0, fileInfo.ChangeTime.Nanoseconds())
|
||||
t.btime.v = time.Unix(0, fileInfo.CreationTime.Nanoseconds())
|
||||
return t, nil
|
||||
}
|
||||
|
||||
const hasPlatformSpecificStat = true
|
||||
|
||||
func platformSpecficStat(name string) (Timespec, error) {
|
||||
if findProcErr != nil {
|
||||
return nil, findProcErr
|
||||
}
|
||||
|
||||
pathp, e := syscall.UTF16PtrFromString(name)
|
||||
if e != nil {
|
||||
return nil, e
|
||||
}
|
||||
h, e := syscall.CreateFile(pathp,
|
||||
syscall.FILE_WRITE_ATTRIBUTES, syscall.FILE_SHARE_WRITE, nil,
|
||||
syscall.OPEN_EXISTING, syscall.FILE_FLAG_BACKUP_SEMANTICS, 0)
|
||||
if e != nil {
|
||||
return nil, e
|
||||
}
|
||||
defer syscall.Close(h)
|
||||
|
||||
return statFile(h)
|
||||
}
|
||||
|
||||
var (
|
||||
findProcErr error
|
||||
procGetFileInformationByHandleEx *syscall.Proc
|
||||
)
|
||||
|
||||
func init() {
|
||||
var modkernel32 *syscall.DLL
|
||||
if modkernel32, findProcErr = syscall.LoadDLL("kernel32.dll"); findProcErr == nil {
|
||||
procGetFileInformationByHandleEx, findProcErr = modkernel32.FindProc("GetFileInformationByHandleEx")
|
||||
}
|
||||
}
|
||||
|
||||
// fileBasicInfo holds the C++ data for FileTimes.
|
||||
//
|
||||
// https://msdn.microsoft.com/en-us/library/windows/desktop/aa364217(v=vs.85).aspx
|
||||
type fileBasicInfo struct {
|
||||
CreationTime syscall.Filetime
|
||||
LastAccessTime syscall.Filetime
|
||||
LastWriteTime syscall.Filetime
|
||||
ChangeTime syscall.Filetime
|
||||
FileAttributes uint32
|
||||
_ uint32 // padding
|
||||
}
|
||||
|
||||
type fileInformationClass int
|
||||
|
||||
const (
|
||||
fileBasicInfoClass fileInformationClass = iota
|
||||
)
|
||||
|
||||
func getFileInformationByHandleEx(handle syscall.Handle, data *fileBasicInfo) (err error) {
|
||||
if findProcErr != nil {
|
||||
return findProcErr
|
||||
}
|
||||
|
||||
r1, _, e1 := syscall.Syscall6(procGetFileInformationByHandleEx.Addr(), 4, uintptr(handle), uintptr(fileBasicInfoClass), uintptr(unsafe.Pointer(data)), unsafe.Sizeof(*data), 0, 0)
|
||||
if r1 == 0 {
|
||||
if e1 != 0 {
|
||||
err = error(e1)
|
||||
} else {
|
||||
err = syscall.EINVAL
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
@ -1,79 +0,0 @@
|
||||
// Package times provides a platform-independent way to get atime, mtime, ctime and btime for files.
|
||||
package times
|
||||
|
||||
import (
|
||||
"os"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Get returns the Timespec for the given FileInfo
|
||||
func Get(fi os.FileInfo) Timespec {
|
||||
return getTimespec(fi)
|
||||
}
|
||||
|
||||
// Stat returns the Timespec for the given filename.
|
||||
func Stat(name string) (Timespec, error) {
|
||||
if hasPlatformSpecificStat {
|
||||
if ts, err := platformSpecficStat(name); err == nil {
|
||||
return ts, nil
|
||||
}
|
||||
}
|
||||
|
||||
fi, err := os.Stat(name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return getTimespec(fi), nil
|
||||
}
|
||||
|
||||
// Timespec provides access to file times.
|
||||
// ChangeTime() panics unless HasChangeTime() is true and
|
||||
// BirthTime() panics unless HasBirthTime() is true.
|
||||
type Timespec interface {
|
||||
ModTime() time.Time
|
||||
AccessTime() time.Time
|
||||
ChangeTime() time.Time
|
||||
BirthTime() time.Time
|
||||
HasChangeTime() bool
|
||||
HasBirthTime() bool
|
||||
}
|
||||
|
||||
type atime struct {
|
||||
v time.Time
|
||||
}
|
||||
|
||||
func (a atime) AccessTime() time.Time { return a.v }
|
||||
|
||||
type ctime struct {
|
||||
v time.Time
|
||||
}
|
||||
|
||||
func (ctime) HasChangeTime() bool { return true }
|
||||
|
||||
func (c ctime) ChangeTime() time.Time { return c.v }
|
||||
|
||||
type mtime struct {
|
||||
v time.Time
|
||||
}
|
||||
|
||||
func (m mtime) ModTime() time.Time { return m.v }
|
||||
|
||||
type btime struct {
|
||||
v time.Time
|
||||
}
|
||||
|
||||
func (btime) HasBirthTime() bool { return true }
|
||||
|
||||
func (b btime) BirthTime() time.Time { return b.v }
|
||||
|
||||
type noctime struct{}
|
||||
|
||||
func (noctime) HasChangeTime() bool { return false }
|
||||
|
||||
func (noctime) ChangeTime() time.Time { panic("ctime not available") }
|
||||
|
||||
type nobtime struct{}
|
||||
|
||||
func (nobtime) HasBirthTime() bool { return false }
|
||||
|
||||
func (nobtime) BirthTime() time.Time { panic("birthtime not available") }
|
||||
@ -1,40 +0,0 @@
|
||||
// Copyright 2009 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// http://golang.org/src/os/stat_darwin.go
|
||||
|
||||
package times
|
||||
|
||||
import (
|
||||
"os"
|
||||
"syscall"
|
||||
"time"
|
||||
)
|
||||
|
||||
// HasChangeTime and HasBirthTime are true if and only if
|
||||
// the target OS supports them.
|
||||
const (
|
||||
HasChangeTime = true
|
||||
HasBirthTime = true
|
||||
)
|
||||
|
||||
type timespec struct {
|
||||
atime
|
||||
mtime
|
||||
ctime
|
||||
btime
|
||||
}
|
||||
|
||||
func timespecToTime(ts syscall.Timespec) time.Time {
|
||||
return time.Unix(int64(ts.Sec), int64(ts.Nsec))
|
||||
}
|
||||
|
||||
func getTimespec(fi os.FileInfo) (t timespec) {
|
||||
stat := fi.Sys().(*syscall.Stat_t)
|
||||
t.atime.v = timespecToTime(stat.Atimespec)
|
||||
t.mtime.v = timespecToTime(stat.Mtimespec)
|
||||
t.ctime.v = timespecToTime(stat.Ctimespec)
|
||||
t.btime.v = timespecToTime(stat.Birthtimespec)
|
||||
return t
|
||||
}
|
||||
@ -1,39 +0,0 @@
|
||||
// Copyright 2009 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// http://golang.org/src/os/stat_dragonfly.go
|
||||
|
||||
package times
|
||||
|
||||
import (
|
||||
"os"
|
||||
"syscall"
|
||||
"time"
|
||||
)
|
||||
|
||||
// HasChangeTime and HasBirthTime are true if and only if
|
||||
// the target OS supports them.
|
||||
const (
|
||||
HasChangeTime = true
|
||||
HasBirthTime = false
|
||||
)
|
||||
|
||||
type timespec struct {
|
||||
atime
|
||||
mtime
|
||||
ctime
|
||||
nobtime
|
||||
}
|
||||
|
||||
func timespecToTime(ts syscall.Timespec) time.Time {
|
||||
return time.Unix(int64(ts.Sec), int64(ts.Nsec))
|
||||
}
|
||||
|
||||
func getTimespec(fi os.FileInfo) (t timespec) {
|
||||
stat := fi.Sys().(*syscall.Stat_t)
|
||||
t.atime.v = timespecToTime(stat.Atim)
|
||||
t.mtime.v = timespecToTime(stat.Mtim)
|
||||
t.ctime.v = timespecToTime(stat.Ctim)
|
||||
return t
|
||||
}
|
||||
@ -1,40 +0,0 @@
|
||||
// Copyright 2009 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// http://golang.org/src/os/stat_freebsd.go
|
||||
|
||||
package times
|
||||
|
||||
import (
|
||||
"os"
|
||||
"syscall"
|
||||
"time"
|
||||
)
|
||||
|
||||
// HasChangeTime and HasBirthTime are true if and only if
|
||||
// the target OS supports them.
|
||||
const (
|
||||
HasChangeTime = true
|
||||
HasBirthTime = true
|
||||
)
|
||||
|
||||
type timespec struct {
|
||||
atime
|
||||
mtime
|
||||
ctime
|
||||
btime
|
||||
}
|
||||
|
||||
func timespecToTime(ts syscall.Timespec) time.Time {
|
||||
return time.Unix(int64(ts.Sec), int64(ts.Nsec))
|
||||
}
|
||||
|
||||
func getTimespec(fi os.FileInfo) (t timespec) {
|
||||
stat := fi.Sys().(*syscall.Stat_t)
|
||||
t.atime.v = timespecToTime(stat.Atimespec)
|
||||
t.mtime.v = timespecToTime(stat.Mtimespec)
|
||||
t.ctime.v = timespecToTime(stat.Ctimespec)
|
||||
t.btime.v = timespecToTime(stat.Birthtimespec)
|
||||
return t
|
||||
}
|
||||
@ -1,39 +0,0 @@
|
||||
// Copyright 2009 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// http://golang.org/src/os/stat_linux.go
|
||||
|
||||
package times
|
||||
|
||||
import (
|
||||
"os"
|
||||
"syscall"
|
||||
"time"
|
||||
)
|
||||
|
||||
// HasChangeTime and HasBirthTime are true if and only if
|
||||
// the target OS supports them.
|
||||
const (
|
||||
HasChangeTime = true
|
||||
HasBirthTime = false
|
||||
)
|
||||
|
||||
type timespec struct {
|
||||
atime
|
||||
mtime
|
||||
ctime
|
||||
nobtime
|
||||
}
|
||||
|
||||
func timespecToTime(ts syscall.Timespec) time.Time {
|
||||
return time.Unix(int64(ts.Sec), int64(ts.Nsec))
|
||||
}
|
||||
|
||||
func getTimespec(fi os.FileInfo) (t timespec) {
|
||||
stat := fi.Sys().(*syscall.Stat_t)
|
||||
t.atime.v = timespecToTime(stat.Atim)
|
||||
t.mtime.v = timespecToTime(stat.Mtim)
|
||||
t.ctime.v = timespecToTime(stat.Ctim)
|
||||
return t
|
||||
}
|
||||
@ -1,39 +0,0 @@
|
||||
// Copyright 2009 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// http://golang.org/src/os/stat_nacl.go
|
||||
|
||||
package times
|
||||
|
||||
import (
|
||||
"os"
|
||||
"syscall"
|
||||
"time"
|
||||
)
|
||||
|
||||
// HasChangeTime and HasBirthTime are true if and only if
|
||||
// the target OS supports them.
|
||||
const (
|
||||
HasChangeTime = true
|
||||
HasBirthTime = false
|
||||
)
|
||||
|
||||
type timespec struct {
|
||||
atime
|
||||
mtime
|
||||
ctime
|
||||
nobtime
|
||||
}
|
||||
|
||||
func timespecToTime(sec, nsec int64) time.Time {
|
||||
return time.Unix(sec, nsec)
|
||||
}
|
||||
|
||||
func getTimespec(fi os.FileInfo) (t timespec) {
|
||||
stat := fi.Sys().(*syscall.Stat_t)
|
||||
t.atime.v = timespecToTime(stat.Atime, stat.AtimeNsec)
|
||||
t.mtime.v = timespecToTime(stat.Mtime, stat.MtimeNsec)
|
||||
t.ctime.v = timespecToTime(stat.Ctime, stat.CtimeNsec)
|
||||
return t
|
||||
}
|
||||
@ -1,40 +0,0 @@
|
||||
// Copyright 2009 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// http://golang.org/src/os/stat_netbsd.go
|
||||
|
||||
package times
|
||||
|
||||
import (
|
||||
"os"
|
||||
"syscall"
|
||||
"time"
|
||||
)
|
||||
|
||||
// HasChangeTime and HasBirthTime are true if and only if
|
||||
// the target OS supports them.
|
||||
const (
|
||||
HasChangeTime = true
|
||||
HasBirthTime = true
|
||||
)
|
||||
|
||||
type timespec struct {
|
||||
atime
|
||||
mtime
|
||||
ctime
|
||||
btime
|
||||
}
|
||||
|
||||
func timespecToTime(ts syscall.Timespec) time.Time {
|
||||
return time.Unix(int64(ts.Sec), int64(ts.Nsec))
|
||||
}
|
||||
|
||||
func getTimespec(fi os.FileInfo) (t timespec) {
|
||||
stat := fi.Sys().(*syscall.Stat_t)
|
||||
t.atime.v = timespecToTime(stat.Atimespec)
|
||||
t.mtime.v = timespecToTime(stat.Mtimespec)
|
||||
t.ctime.v = timespecToTime(stat.Ctimespec)
|
||||
t.btime.v = timespecToTime(stat.Birthtimespec)
|
||||
return t
|
||||
}
|
||||
@ -1,39 +0,0 @@
|
||||
// Copyright 2009 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// http://golang.org/src/os/stat_openbsd.go
|
||||
|
||||
package times
|
||||
|
||||
import (
|
||||
"os"
|
||||
"syscall"
|
||||
"time"
|
||||
)
|
||||
|
||||
// HasChangeTime and HasBirthTime are true if and only if
|
||||
// the target OS supports them.
|
||||
const (
|
||||
HasChangeTime = true
|
||||
HasBirthTime = false
|
||||
)
|
||||
|
||||
type timespec struct {
|
||||
atime
|
||||
mtime
|
||||
ctime
|
||||
nobtime
|
||||
}
|
||||
|
||||
func timespecToTime(ts syscall.Timespec) time.Time {
|
||||
return time.Unix(int64(ts.Sec), int64(ts.Nsec))
|
||||
}
|
||||
|
||||
func getTimespec(fi os.FileInfo) (t timespec) {
|
||||
stat := fi.Sys().(*syscall.Stat_t)
|
||||
t.atime.v = timespecToTime(stat.Atim)
|
||||
t.mtime.v = timespecToTime(stat.Mtim)
|
||||
t.ctime.v = timespecToTime(stat.Ctim)
|
||||
return t
|
||||
}
|
||||
@ -1,34 +0,0 @@
|
||||
// Copyright 2009 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// http://golang.org/src/os/stat_plan9.go
|
||||
|
||||
package times
|
||||
|
||||
import (
|
||||
"os"
|
||||
"syscall"
|
||||
"time"
|
||||
)
|
||||
|
||||
// HasChangeTime and HasBirthTime are true if and only if
|
||||
// the target OS supports them.
|
||||
const (
|
||||
HasChangeTime = false
|
||||
HasBirthTime = false
|
||||
)
|
||||
|
||||
type timespec struct {
|
||||
atime
|
||||
mtime
|
||||
noctime
|
||||
nobtime
|
||||
}
|
||||
|
||||
func getTimespec(fi os.FileInfo) (t timespec) {
|
||||
stat := fi.Sys().(*syscall.Dir)
|
||||
t.atime.v = time.Unix(int64(stat.Atime), 0)
|
||||
t.mtime.v = time.Unix(int64(stat.Mtime), 0)
|
||||
return t
|
||||
}
|
||||
@ -1,39 +0,0 @@
|
||||
// Copyright 2009 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// http://golang.org/src/os/stat_solaris.go
|
||||
|
||||
package times
|
||||
|
||||
import (
|
||||
"os"
|
||||
"syscall"
|
||||
"time"
|
||||
)
|
||||
|
||||
// HasChangeTime and HasBirthTime are true if and only if
|
||||
// the target OS supports them.
|
||||
const (
|
||||
HasChangeTime = true
|
||||
HasBirthTime = false
|
||||
)
|
||||
|
||||
type timespec struct {
|
||||
atime
|
||||
mtime
|
||||
ctime
|
||||
nobtime
|
||||
}
|
||||
|
||||
func timespecToTime(ts syscall.Timespec) time.Time {
|
||||
return time.Unix(int64(ts.Sec), int64(ts.Nsec))
|
||||
}
|
||||
|
||||
func getTimespec(fi os.FileInfo) (t timespec) {
|
||||
stat := fi.Sys().(*syscall.Stat_t)
|
||||
t.atime.v = timespecToTime(stat.Atim)
|
||||
t.mtime.v = timespecToTime(stat.Mtim)
|
||||
t.ctime.v = timespecToTime(stat.Ctim)
|
||||
return t
|
||||
}
|
||||
@ -1,65 +0,0 @@
|
||||
package times
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestStat(t *testing.T) {
|
||||
fileTest(t, func(f *os.File) {
|
||||
ts, err := Stat(f.Name())
|
||||
if err != nil {
|
||||
t.Error(err.Error())
|
||||
}
|
||||
timespecTest(ts, newInterval(time.Now(), time.Second), t)
|
||||
})
|
||||
}
|
||||
|
||||
func TestGet(t *testing.T) {
|
||||
fileTest(t, func(f *os.File) {
|
||||
fi, err := os.Stat(f.Name())
|
||||
if err != nil {
|
||||
t.Error(err.Error())
|
||||
}
|
||||
timespecTest(Get(fi), newInterval(time.Now(), time.Second), t)
|
||||
})
|
||||
}
|
||||
|
||||
func TestStatErr(t *testing.T) {
|
||||
_, err := Stat("badfile?")
|
||||
if err == nil {
|
||||
t.Error("expected an error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheat(t *testing.T) {
|
||||
// not all times are available for all platforms
|
||||
// this allows us to get 100% test coverage for platforms which do not have
|
||||
// ChangeTime/BirthTime
|
||||
var c ctime
|
||||
if c.HasChangeTime() {
|
||||
c.ChangeTime()
|
||||
}
|
||||
|
||||
var b btime
|
||||
if b.HasBirthTime() {
|
||||
b.BirthTime()
|
||||
}
|
||||
|
||||
var nc noctime
|
||||
func() {
|
||||
if !nc.HasChangeTime() {
|
||||
defer func() { recover() }()
|
||||
}
|
||||
nc.ChangeTime()
|
||||
}()
|
||||
|
||||
var nb nobtime
|
||||
func() {
|
||||
if !nb.HasBirthTime() {
|
||||
defer func() { recover() }()
|
||||
}
|
||||
nb.BirthTime()
|
||||
}()
|
||||
}
|
||||
@ -1,36 +0,0 @@
|
||||
// Copyright 2009 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// http://golang.org/src/os/stat_windows.go
|
||||
|
||||
package times
|
||||
|
||||
import (
|
||||
"os"
|
||||
"syscall"
|
||||
"time"
|
||||
)
|
||||
|
||||
// HasChangeTime and HasBirthTime are true if and only if
|
||||
// the target OS supports them.
|
||||
const (
|
||||
HasChangeTime = false
|
||||
HasBirthTime = true
|
||||
)
|
||||
|
||||
type timespec struct {
|
||||
atime
|
||||
mtime
|
||||
noctime
|
||||
btime
|
||||
}
|
||||
|
||||
func getTimespec(fi os.FileInfo) Timespec {
|
||||
var t timespec
|
||||
stat := fi.Sys().(*syscall.Win32FileAttributeData)
|
||||
t.atime.v = time.Unix(0, stat.LastAccessTime.Nanoseconds())
|
||||
t.mtime.v = time.Unix(0, stat.LastWriteTime.Nanoseconds())
|
||||
t.btime.v = time.Unix(0, stat.CreationTime.Nanoseconds())
|
||||
return t
|
||||
}
|
||||
@ -1,10 +0,0 @@
|
||||
// +build !windows
|
||||
|
||||
package times
|
||||
|
||||
const hasPlatformSpecificStat = false
|
||||
|
||||
// do not use, only here to prevent "undefined" method error.
|
||||
func platformSpecficStat(name string) (Timespec, error) {
|
||||
return nil, nil
|
||||
}
|
||||
@ -1,50 +0,0 @@
|
||||
package times
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
type timeRange struct {
|
||||
start time.Time
|
||||
end time.Time
|
||||
}
|
||||
|
||||
func newInterval(t time.Time, dur time.Duration) timeRange {
|
||||
return timeRange{start: t.Add(-dur), end: t.Add(dur)}
|
||||
}
|
||||
|
||||
func (t timeRange) Contains(findTime time.Time) bool {
|
||||
return !findTime.Before(t.start) && !findTime.After(t.end)
|
||||
}
|
||||
|
||||
// creates a file and cleans it up after the test is run.
|
||||
func fileTest(t testing.TB, testFunc func(f *os.File)) {
|
||||
f, err := ioutil.TempFile("", "")
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
defer os.Remove(f.Name())
|
||||
defer f.Close()
|
||||
testFunc(f)
|
||||
}
|
||||
|
||||
func timespecTest(ts Timespec, r timeRange, t testing.TB) {
|
||||
if !r.Contains(ts.AccessTime()) {
|
||||
t.Errorf("expected %s to be in range: %s\n", ts.AccessTime(), r.start)
|
||||
}
|
||||
|
||||
if !r.Contains(ts.ModTime()) {
|
||||
t.Errorf("expected %s to be in range: %s\n", ts.ModTime(), r.start)
|
||||
}
|
||||
|
||||
if ts.HasChangeTime() && !r.Contains(ts.ChangeTime()) {
|
||||
t.Errorf("expected %s to be in range: %s\n", ts.ChangeTime(), r.start)
|
||||
}
|
||||
|
||||
if ts.HasBirthTime() && !r.Contains(ts.BirthTime()) {
|
||||
t.Errorf("expected %s to be in range: %s\n", ts.BirthTime(), r.start)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user