summaryrefslogtreecommitdiff
path: root/internal/client/output/local_unix.go
blob: dd53e79c9230ff36aa79a67f66b65df1c32e8541 (plain)
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
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
package output

import (
	"fmt"
	"io"
	"log/slog"
	"mime"
	"os"
	"strconv"
	"strings"
	"syscall"
	"time"

	"github.com/SayaAndy/saya-today-thumbnail-generator/config"
	"github.com/SayaAndy/saya-today-thumbnail-generator/internal/client/input"
	"golang.org/x/sys/unix"
)

var _ OutputClient = (*LocalUnixOutputClient)(nil)

type LocalUnixOutputClient struct {
	path     string
	fileMode uint32
	dirMode  uint32
	attrMode string
}

func NewLocalUnixOutputClient(cfg *config.OutputConfig) (OutputClient, error) {
	if cfg.Storage.Type != "local-unix" {
		return nil, fmt.Errorf("invalid storage type for LocalUnixOutputClient")
	}
	localCfg := cfg.Storage.Config.(*config.OutputLocalUnixConfig)

	fpm, err := strconv.ParseInt(localCfg.FilePermissionMode, 8, 32)
	if err != nil {
		return nil, fmt.Errorf("fail to parse file permission mode as an octal number: %w", err)
	}

	dpm, err := strconv.ParseInt(localCfg.DirPermissionMode, 8, 32)
	if err != nil {
		return nil, fmt.Errorf("fail to parse directory permission mode as an octal number: %w", err)
	}

	return &LocalUnixOutputClient{localCfg.Path, uint32(fpm), uint32(dpm), localCfg.AttributesImplementation}, nil
}

func (c *LocalUnixOutputClient) GetWriter(path string, inputMetadata *input.MetadataStruct) (io.WriteCloser, error) {
	pathSegments := strings.Split(path, "/")
	dirpath := strings.Join(pathSegments[0:len(pathSegments)-1], "/")
	if err := os.MkdirAll(c.path+dirpath, os.FileMode(c.dirMode)); err != nil {
		return nil, fmt.Errorf("fail to mkdir parent directories for a path: %w", err)
	}

	var err error
	if _, err = os.Stat(c.path + path); err != nil && !os.IsNotExist(err) {
		return nil, fmt.Errorf("fail to stat a file, although file exists: %w", err)
	}
	if os.IsNotExist(err) {
		f, err := os.OpenFile(c.path+path, os.O_WRONLY|os.O_CREATE, os.FileMode(c.fileMode))
		if err != nil {
			return nil, fmt.Errorf("fail to create a file: %w", err)
		}
		f.Close()
	}

	switch c.attrMode {
	case "xattr":
		if err := unix.Setxattr(c.path+path, "user.originalfile.mddate", []byte(strconv.FormatInt(inputMetadata.LastModified.Unix(), 16)), 0); err != nil {
			return nil, fmt.Errorf("fail to write user.originalfile.mddate xattribute: %w", err)
		}
	case "none":
	default:
		return nil, fmt.Errorf("unknown attributes implementation: %s", c.attrMode)
	}

	return os.OpenFile(c.path+path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, os.FileMode(c.fileMode))
}

func (c *LocalUnixOutputClient) ReadMetadata(path string) (*MetadataStruct, error) {
	nodePathParts := strings.Split(path, "/")
	nodeName := nodePathParts[len(nodePathParts)-1]
	nodeNameParts := strings.Split(nodeName, ".")
	nodeExt := ""
	if len(nodeNameParts) >= 2 {
		nodeExt = nodeNameParts[len(nodeNameParts)-1]
	}

	fileInfo, err := os.Stat(c.path + path)
	if err != nil {
		return nil, fmt.Errorf("fail to read file info: %w", err)
	}

	stat_t := fileInfo.Sys().(*syscall.Stat_t)
	creationTime := time.Unix(stat_t.Ctim.Sec, stat_t.Ctim.Nsec)

	mddateOriginal := make([]byte, 0)
	switch c.attrMode {
	case "xattr":
		sz, err := unix.Getxattr(c.path+path, "user.originalfile.mddate", nil)
		if err != nil {
			slog.Warn("fail to get size of user.originalfile.mddate attribute, proceeding as if no such attribute is there", slog.String("error", err.Error()))
			break
		}
		mddateOriginal = make([]byte, sz)
		if _, err = unix.Getxattr(c.path+path, "user.originalfile.mddate", mddateOriginal); err != nil {
			return nil, fmt.Errorf("fail to get user.originalfile.mddate attribute: %w", err)
		}
	case "none":
	default:
		return nil, fmt.Errorf("unknown attributes implementation: %s", c.attrMode)
	}

	return &MetadataStruct{
		Name:         fileInfo.Name(),
		StorageType:  "local-unix",
		Hash:         strconv.FormatInt(fileInfo.ModTime().Unix(), 16),
		HashOriginal: string(mddateOriginal),
		ContentType:  mime.TypeByExtension("." + nodeExt),
		FirstCreated: creationTime,
		LastModified: fileInfo.ModTime(),
		Size:         fileInfo.Size(),
		Misc:         map[string]string{},
	}, nil
}

func (c *LocalUnixOutputClient) IsMissing(path string) bool {
	_, err := os.Stat(c.path + path)
	return err != nil
}