Import first working version
[fuse-aexplorer.git] / AEpy / AECmds.py
1 import binascii
2 import serial
3 from stat import S_IRUSR, S_IWUSR, S_IXUSR, S_IRGRP, S_IXGRP, S_IROTH, S_IXOTH, S_IFDIR, S_IFREG
4 import struct
5
6 from AESession import AESession
7 import AEMultipart
8
9
10 ###################################
11 #
12 # All paths are without leading /
13 # All paths are in Amiga charset.
14 #
15 ###################################
16
17
18 # Command 0x01
19 # Cancel a running operation.
20 #
21 def cancel(session):
22     session.sendMsg(1, b'')
23
24
25
26 # Command 0x64
27 # List a directory.
28 #
29 def dir_list(session, path):
30     print("AECmds.dir_list: " + path)
31
32     session.sendMsg(0x64, path + b'\x00\x01')
33
34     dirlist = AEMultipart.recv(session)
35     # TODO: Check for None returned, meaning that the path doesn't exist.
36     #       Actually, what can we return then?
37     #       None is not iterable, and [] indicates an empty directory.
38     close(session)
39
40
41     # Parse dirlist
42
43     (numents,) = struct.unpack('!L', dirlist[0:4])
44     dirlist = dirlist[4:]
45
46     kids = {}
47     #kids['.'] = (dict(st_mode=(S_IFDIR | 0o755), st_nlink=2), {})
48     #kids['..'] = (dict(st_mode=(S_IFDIR | 0o755), st_nlink=2), {})
49
50     while numents > 0:
51         assert len(dirlist) > 29
52         (entlen, size, used, type, attrs, mdays, mmins, mticks, type2)  = struct.unpack('!LLLHHLLLB', dirlist[0:29])
53         # entlen  - Size of this entry in the list, in bytes
54         # size    - Size of file.
55         #           For drives, total space.
56         # used    - 0 for files and folders.
57         #           For drives, space used.
58         # type    - ?
59         # attrs   - Amiga attributes
60         # mdays   - Last modification in days since 1978-01-01
61         # mmins   - Last modification in minutes since mdays
62         # mticks  - Last modification in ticks (1/50 sec) since mmins
63         # type2   - 0: Volume
64         #         - 1: Volume
65         #         - 2: Directory
66         #         - 3: File
67         #         - 4: ROM
68         #         - 5: ADF
69         #         - 6: HDF
70         assert entlen <= len(dirlist)
71
72         name = dirlist[29:entlen].split(b'\x00')[0].decode(encoding="iso8859-1")
73
74         dirlist = dirlist[entlen:]
75         numents -= 1
76
77         st = {}
78         st = {}
79         st['st_mode'] = 0
80         #if attrs & 0x40: st['st_mode'] |= Script
81         #if attrs & 0x20: st['st_mode'] |= Pure
82         #if attrs & 0x10: st['st_mode'] |= Archive
83         if not attrs & 0x08: st['st_mode'] |= S_IRUSR | S_IRGRP | S_IROTH # Read
84         if not attrs & 0x04: st['st_mode'] |= S_IWUSR # Write
85         if not attrs & 0x02: st['st_mode'] |= S_IXUSR | S_IXGRP | S_IXOTH # Execute
86         #if not attrs & 0x01: st['st_mode'] |=  Delete
87
88         # Check for directory
89         if type & 0x8000:
90             st['st_mode'] |= S_IFDIR
91         else:
92             st['st_mode'] |= S_IFREG
93             st['st_nlink'] = 1
94
95         st['st_size'] = size
96
97         st['st_mtime']  = (2922*24*60*60)   # Amiga epoch starts 1978-01-01
98         st['st_mtime'] += (mdays*24*60*60)  # Days since 1978-01-01
99         st['st_mtime'] += (mmins*60)        # Minutes
100         st['st_mtime'] += (mticks/50)       # Ticks of 1/50 sec
101
102         st['st_blksize'] = session.packetsize - 4
103
104         # TODO: Convert time zone.
105         # Amiga time seems to be local time, we currently treat it as UTC.
106
107         kids[name] = (st, None)
108
109     return kids
110
111
112
113 # Command 0x65
114 # Read a file.
115 #
116 def file_read_start(session, path):
117     print("AECmds.file_read_start: " + path)
118
119     # Request file
120     session.sendMsg(0x65, path + b'\x00')
121
122     # Get file response header
123     (filelen, pktsize) = AEMultipart.recvHeader(session)
124
125     # TODO: Check for None returned, meaning that the path doesn't exist.
126     # This may not be necessary in case FUSE always issues a getattr()
127     # beforehand.
128
129     return filelen
130
131
132 def file_read_next(session):
133     (blockOffset, blockData) = AEMultipart.recvBlock(session)
134
135     if (blockData == None):
136         blen = None
137     else:
138         blen = len(blockData)
139
140     print("AECmds.file_read_next: " + str(blen) + ' @ ' + str(blockOffset))
141
142     return (blockOffset, blockData)
143
144
145
146 # Command 0x66
147 # Write a complete file and set its attributes and time.
148 #
149 def file_write(session, path, amigaattrs, unixtime, filebody):
150     print('AECmds.file_write: ' + path + ' -- ' + str(len(filebody)))
151
152     filelen = len(filebody)
153
154     _utime = unixtime - (2922*24*60*60)  # Amiga epoch starts 1978-01-01
155     mdays = int(_utime / (24*60*60))     # Days since 1978-01-01
156     _utime -= mdays * (24*60*60)
157     mmins = int(_utime / 60)             # Minutes
158     _utime -= mmins * 60
159     mticks = _utime * 50                 # Ticks of 1/50 sec
160
161     filetype = 3 # Regular file
162
163     data = struct.pack('!LLLLLLLB',
164                        29 + len(path) + 6,  # Length of this message, really
165                        filelen,
166                        0,
167                        amigaattrs,
168                        mdays,
169                        mmins,
170                        mticks,
171                        filetype)
172     data += path
173     data += b'\0\0\0\0\0\0'  # No idea what this is for
174     session.sendMsg(0x66, data)
175
176     # TODO: Handle target FS full
177     # TODO: Handle permission denied on target FS
178
179     AEMultipart.send(session, filebody)
180     close(session)
181
182
183
184 # Command 0x67
185 # Delete a path.
186 # Can be a file or folder, and will be deleted recursively.
187 #
188 def delete(session, path):
189     print("AECmds.delete: " + path)
190
191     session.sendMsg(0x67, path + b'\x00')
192
193     (type, _) = session.recvMsg()
194
195     close(session)
196
197     # type == 0 means the file was deleted successfully
198     return (type == 0)
199
200
201
202 # Command 0x68
203 # Rename a file, leaving it in the same folder.
204 #
205 def rename(session, path, new_name):
206     print("AECmds.rename: " + path + ' - ' + new_name)
207     session.sendMsg(0x68, path + b'\x00' + new_name + b'\x00')
208     (type, _) = session.recvMsg()
209
210     if type != 0:
211         print("AECmds.rename: Response type " + str(type))
212
213     close(session)
214
215     # Assume that type == 0 means the file was renamed successfully
216     return (type == 0)
217
218
219
220 # Command 0x69
221 # Move a file from a folder to a different one, keeping its name.
222 #
223 def move(session, path, new_parent):
224     print("AECmds.move: " + path + ' - ' + new_parent)
225     session.sendMsg(0x69, path + b'\x00' + new_parent + b'\x00\xc9')
226     (type, _) = session.recvMsg()
227
228     if type != 0:
229         print("AECmds.move: Response type " + str(type))
230
231     close(session)
232
233     # Assume that type == 0 means the file was moved successfully
234     return (type == 0)
235
236
237
238 # Command 0x6a
239 # Copy a file.
240 #
241 def copy(session, path, new_parent):
242     print("AECmds.copy: " + path + ' - ' + new_parent)
243     session.sendMsg(0x6a, path + b'\x00' + new_parent + b'\x00\xc9')
244     (type, _) = session.recvMsg()
245
246     if type != 0:
247         print("AECmds.copy: Response type " + str(type))
248
249     close(session)
250
251     # Assume that type == 0 means the file was copied successfully
252     return (type == 0)
253
254
255
256 # Command 0x6b
257 # Set attributes and comment.
258 #
259 def setattr(session, amigaattrs, path, comment):
260     print("AECmds.setattr: " + str(amigaattrs) + ' - ' + path + ' - ' + comment)
261
262     data = struct.pack('!L', amigaattrs)
263     data += path + b'\x00'
264     data += comment + b'\x00'
265     data += struct.pack('!L', 0)
266
267     session.sendMsg(0x6b, data)
268     (type, _) = session.recvMsg()
269
270     if type != 0:
271         print("AECmds.setattr: Response type " + str(type))
272
273     close(session)
274
275     # Assume that type == 0 means the file was copied successfully
276     return (type == 0)
277
278
279
280 # Command 0x6c
281 # Request (?)
282 #
283
284
285
286 # Command 0x6d
287 # Finish a running operation.
288 #
289 def close(session):
290     print('AECmds.close')
291
292     session.sendMsg(0x6d, b'')
293
294     (type, data) = session.recvMsg()
295     assert type == 0x0a
296     if data != b'\0\0\0\0\0':
297         print('AECmds.close: data == ' + binascii.hexlify(data))
298     # Format of data returned:
299     # Byte 0: ?
300     # Byte 1: ?
301     # Byte 2: ?
302     # Byte 3: 00 - No error
303     #         06 - File no longer exists
304     #         1c - Timed out waiting for host to request next read block
305     # Byte 4: Path in case of error 06.
306     #         Empty (null-terminated) otherwise?
307     #         Null-terminated string.
308     #assert data == b'\0\0\0\0\0'
309
310
311
312 # Command 0x6e
313 # Format a disk.
314 #
315
316
317
318 # Command 0x6f
319 # Create a new folder and matching .info file.
320 # This folder will be named "Unnamed1".
321 #