]> git.r.bdr.sh - rbdr/mobius/blob - hotline/file_wrapper.go
Replace hardcoded version with ldflag usage
[rbdr/mobius] / hotline / file_wrapper.go
1 package hotline
2
3 import (
4 "bytes"
5 "encoding/binary"
6 "errors"
7 "fmt"
8 "io"
9 "io/fs"
10 "os"
11 "path/filepath"
12 )
13
14 const (
15 incompleteFileSuffix = ".incomplete"
16 infoForkNameTemplate = ".info_%s" // template string for info fork filenames
17 rsrcForkNameTemplate = ".rsrc_%s" // template string for resource fork filenames
18 )
19
20 // fileWrapper encapsulates the data, info, and resource forks of a Hotline file and provides methods to manage the files.
21 type fileWrapper struct {
22 fs FileStore
23 name string // name of the file
24 path string // path to file directory
25 dataPath string // path to the file data fork
26 dataOffset int64
27 rsrcPath string // path to the file resource fork
28 infoPath string // path to the file information fork
29 incompletePath string // path to partially transferred temp file
30 ffo *flattenedFileObject
31 }
32
33 func newFileWrapper(fs FileStore, path string, dataOffset int64) (*fileWrapper, error) {
34 dir := filepath.Dir(path)
35 fName := filepath.Base(path)
36 f := fileWrapper{
37 fs: fs,
38 name: fName,
39 path: dir,
40 dataPath: path,
41 dataOffset: dataOffset,
42 rsrcPath: filepath.Join(dir, fmt.Sprintf(rsrcForkNameTemplate, fName)),
43 infoPath: filepath.Join(dir, fmt.Sprintf(infoForkNameTemplate, fName)),
44 incompletePath: filepath.Join(dir, fName+incompleteFileSuffix),
45 ffo: &flattenedFileObject{},
46 }
47
48 var err error
49 f.ffo, err = f.flattenedFileObject()
50 if err != nil {
51 return nil, err
52 }
53
54 return &f, nil
55 }
56
57 func (f *fileWrapper) totalSize() []byte {
58 var s int64
59 size := make([]byte, 4)
60
61 info, err := f.fs.Stat(f.dataPath)
62 if err == nil {
63 s += info.Size() - f.dataOffset
64 }
65
66 info, err = f.fs.Stat(f.rsrcPath)
67 if err == nil {
68 s += info.Size()
69 }
70
71 binary.BigEndian.PutUint32(size, uint32(s))
72
73 return size
74 }
75
76 func (f *fileWrapper) rsrcForkSize() (s [4]byte) {
77 info, err := f.fs.Stat(f.rsrcPath)
78 if err != nil {
79 return s
80 }
81
82 binary.BigEndian.PutUint32(s[:], uint32(info.Size()))
83 return s
84 }
85
86 func (f *fileWrapper) rsrcForkHeader() FlatFileForkHeader {
87 return FlatFileForkHeader{
88 ForkType: [4]byte{0x4D, 0x41, 0x43, 0x52}, // "MACR"
89 CompressionType: [4]byte{},
90 RSVD: [4]byte{},
91 DataSize: f.rsrcForkSize(),
92 }
93 }
94
95 func (f *fileWrapper) incompleteDataName() string {
96 return f.name + incompleteFileSuffix
97 }
98
99 func (f *fileWrapper) rsrcForkName() string {
100 return fmt.Sprintf(rsrcForkNameTemplate, f.name)
101 }
102
103 func (f *fileWrapper) infoForkName() string {
104 return fmt.Sprintf(infoForkNameTemplate, f.name)
105 }
106
107 func (f *fileWrapper) rsrcForkWriter() (io.WriteCloser, error) {
108 file, err := os.OpenFile(f.rsrcPath, os.O_CREATE|os.O_WRONLY, 0644)
109 if err != nil {
110 return nil, err
111 }
112
113 return file, nil
114 }
115
116 func (f *fileWrapper) infoForkWriter() (io.WriteCloser, error) {
117 file, err := os.OpenFile(f.infoPath, os.O_CREATE|os.O_WRONLY, 0644)
118 if err != nil {
119 return nil, err
120 }
121
122 return file, nil
123 }
124
125 func (f *fileWrapper) incFileWriter() (io.WriteCloser, error) {
126 file, err := os.OpenFile(f.incompletePath, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644)
127 if err != nil {
128 return nil, err
129 }
130
131 return file, nil
132 }
133
134 func (f *fileWrapper) dataForkReader() (io.Reader, error) {
135 return f.fs.Open(f.dataPath)
136 }
137
138 func (f *fileWrapper) rsrcForkFile() (*os.File, error) {
139 return f.fs.Open(f.rsrcPath)
140 }
141
142 func (f *fileWrapper) dataFile() (os.FileInfo, error) {
143 if fi, err := f.fs.Stat(f.dataPath); err == nil {
144 return fi, nil
145 }
146 if fi, err := f.fs.Stat(f.incompletePath); err == nil {
147 return fi, nil
148 }
149
150 return nil, errors.New("file or directory not found")
151 }
152
153 // move a fileWrapper and its associated meta files to newPath.
154 // Meta files include:
155 // * Partially uploaded file ending with .incomplete
156 // * Resource fork starting with .rsrc_
157 // * Info fork starting with .info
158 // During move of the meta files, os.ErrNotExist is ignored as these files may legitimately not exist.
159 func (f *fileWrapper) move(newPath string) error {
160 err := f.fs.Rename(f.dataPath, filepath.Join(newPath, f.name))
161 if err != nil {
162 return err
163 }
164
165 err = f.fs.Rename(f.incompletePath, filepath.Join(newPath, f.incompleteDataName()))
166 if err != nil && !errors.Is(err, os.ErrNotExist) {
167 return err
168 }
169
170 err = f.fs.Rename(f.rsrcPath, filepath.Join(newPath, f.rsrcForkName()))
171 if err != nil && !errors.Is(err, os.ErrNotExist) {
172 return err
173 }
174
175 err = f.fs.Rename(f.infoPath, filepath.Join(newPath, f.infoForkName()))
176 if err != nil && !errors.Is(err, os.ErrNotExist) {
177 return err
178 }
179
180 return nil
181 }
182
183 // delete a fileWrapper and its associated metadata files if they exist
184 func (f *fileWrapper) delete() error {
185 err := f.fs.RemoveAll(f.dataPath)
186 if err != nil {
187 return err
188 }
189
190 err = f.fs.Remove(f.incompletePath)
191 if err != nil && !errors.Is(err, os.ErrNotExist) {
192 return err
193 }
194
195 err = f.fs.Remove(f.rsrcPath)
196 if err != nil && !errors.Is(err, os.ErrNotExist) {
197 return err
198 }
199
200 err = f.fs.Remove(f.infoPath)
201 if err != nil && !errors.Is(err, os.ErrNotExist) {
202 return err
203 }
204
205 return nil
206 }
207
208 func (f *fileWrapper) flattenedFileObject() (*flattenedFileObject, error) {
209 dataSize := make([]byte, 4)
210 mTime := [8]byte{}
211
212 ft := defaultFileType
213
214 fileInfo, err := f.fs.Stat(f.dataPath)
215 if err != nil && !errors.Is(err, fs.ErrNotExist) {
216 return nil, err
217 }
218 if errors.Is(err, fs.ErrNotExist) {
219 fileInfo, err = f.fs.Stat(f.incompletePath)
220 if err == nil {
221 mTime = toHotlineTime(fileInfo.ModTime())
222 binary.BigEndian.PutUint32(dataSize, uint32(fileInfo.Size()-f.dataOffset))
223 ft, _ = fileTypeFromInfo(fileInfo)
224 }
225 } else {
226 mTime = toHotlineTime(fileInfo.ModTime())
227 binary.BigEndian.PutUint32(dataSize, uint32(fileInfo.Size()-f.dataOffset))
228 ft, _ = fileTypeFromInfo(fileInfo)
229 }
230
231 f.ffo.FlatFileHeader = FlatFileHeader{
232 Format: [4]byte{0x46, 0x49, 0x4c, 0x50}, // "FILP"
233 Version: [2]byte{0, 1},
234 RSVD: [16]byte{},
235 ForkCount: [2]byte{0, 2},
236 }
237
238 _, err = f.fs.Stat(f.infoPath)
239 if err == nil {
240 b, err := f.fs.ReadFile(f.infoPath)
241 if err != nil {
242 return nil, err
243 }
244
245 f.ffo.FlatFileHeader.ForkCount[1] = 3
246
247 _, err = io.Copy(&f.ffo.FlatFileInformationFork, bytes.NewReader(b))
248 if err != nil {
249 return nil, err
250 }
251
252 } else {
253 f.ffo.FlatFileInformationFork = FlatFileInformationFork{
254 Platform: []byte("AMAC"), // TODO: Remove hardcode to support "AWIN" Platform (maybe?)
255 TypeSignature: []byte(ft.TypeCode),
256 CreatorSignature: []byte(ft.CreatorCode),
257 Flags: []byte{0, 0, 0, 0},
258 PlatformFlags: []byte{0, 0, 1, 0}, // TODO: What is this?
259 RSVD: make([]byte, 32),
260 CreateDate: mTime[:], // some filesystems don't support createTime
261 ModifyDate: mTime[:],
262 NameScript: []byte{0, 0},
263 Name: []byte(f.name),
264 NameSize: []byte{0, 0},
265 CommentSize: []byte{0, 0},
266 Comment: []byte{},
267 }
268 binary.BigEndian.PutUint16(f.ffo.FlatFileInformationFork.NameSize, uint16(len(f.name)))
269 }
270
271 f.ffo.FlatFileInformationForkHeader = FlatFileForkHeader{
272 ForkType: [4]byte{0x49, 0x4E, 0x46, 0x4F}, // "INFO"
273 CompressionType: [4]byte{},
274 RSVD: [4]byte{},
275 DataSize: f.ffo.FlatFileInformationFork.Size(),
276 }
277
278 f.ffo.FlatFileDataForkHeader = FlatFileForkHeader{
279 ForkType: [4]byte{0x44, 0x41, 0x54, 0x41}, // "DATA"
280 CompressionType: [4]byte{},
281 RSVD: [4]byte{},
282 DataSize: [4]byte{dataSize[0], dataSize[1], dataSize[2], dataSize[3]},
283 }
284 f.ffo.FlatFileResForkHeader = f.rsrcForkHeader()
285
286 return f.ffo, nil
287 }