]> git.sesse.net Git - pistorm/blob - a314/files_pi/a314fs.py
Fixes to allow a314 configuration on non default paths
[pistorm] / a314 / files_pi / a314fs.py
1 #!/usr/bin/python3
2 # -*- coding: utf-8 -*-
3
4 # Copyright (c) 2018-2021 Niklas Ekström
5
6 import select
7 import sys
8 import socket
9 import time
10 import os
11 import struct
12 import glob
13 import logging
14 import json
15
16 logging.basicConfig(format = '%(levelname)s, %(asctime)s, %(name)s, line %(lineno)d: %(message)s')
17 logger = logging.getLogger(__name__)
18 logger.setLevel(logging.INFO)
19
20 try:
21     idx = sys.argv.index('-conf-file')
22     CONFIG_FILE_PATH = sys.argv[idx + 1]
23 except (ValueError, IndexError):
24     CONFIG_FILE_PATH = 'a314/files_pi/a314fs.conf'
25
26 SHARED_DIRECTORY = 'data/a314shared'
27 METAFILE_EXTENSION = ':a314'
28
29 with open(CONFIG_FILE_PATH, encoding='utf-8') as f:
30     cfg = json.load(f)
31     devs = cfg['devices']
32     dev = devs['PI0']
33     SHARED_DIRECTORY = dev['path']
34
35 MSG_REGISTER_REQ        = 1
36 MSG_REGISTER_RES        = 2
37 MSG_DEREGISTER_REQ      = 3
38 MSG_DEREGISTER_RES      = 4
39 MSG_READ_MEM_REQ        = 5
40 MSG_READ_MEM_RES        = 6
41 MSG_WRITE_MEM_REQ       = 7
42 MSG_WRITE_MEM_RES       = 8
43 MSG_CONNECT             = 9
44 MSG_CONNECT_RESPONSE    = 10
45 MSG_DATA                = 11
46 MSG_EOS                 = 12
47 MSG_RESET               = 13
48
49 def wait_for_msg():
50     header = b''
51     while len(header) < 9:
52         data = drv.recv(9 - len(header))
53         if not data:
54             logger.error('Connection to a314d was closed, terminating.')
55             exit(-1)
56         header += data
57     (plen, stream_id, ptype) = struct.unpack('=IIB', header)
58     payload = b''
59     while len(payload) < plen:
60         data = drv.recv(plen - len(payload))
61         if not data:
62             logger.error('Connection to a314d was closed, terminating.')
63             exit(-1)
64         payload += data
65     return (stream_id, ptype, payload)
66
67 def send_register_req(name):
68     m = struct.pack('=IIB', len(name), 0, MSG_REGISTER_REQ) + name
69     drv.sendall(m)
70
71 def send_read_mem_req(address, length):
72     m = struct.pack('=IIBII', 8, 0, MSG_READ_MEM_REQ, address, length)
73     drv.sendall(m)
74
75 def read_mem(address, length):
76     send_read_mem_req(address, length)
77     stream_id, ptype, payload = wait_for_msg()
78     if ptype != MSG_READ_MEM_RES:
79         logger.error('Expected MSG_READ_MEM_RES but got %s. Shutting down.', ptype)
80         exit(-1)
81     return payload
82
83 def send_write_mem_req(address, data):
84     m = struct.pack('=IIBI', 4 + len(data), 0, MSG_WRITE_MEM_REQ, address) + data
85     drv.sendall(m)
86
87 def write_mem(address, data):
88     send_write_mem_req(address, data)
89     stream_id, ptype, payload = wait_for_msg()
90     if ptype != MSG_WRITE_MEM_RES:
91         logger.error('Expected MSG_WRITE_MEM_RES but got %s. Shutting down.', ptype)
92         exit(-1)
93
94 def send_connect_response(stream_id, result):
95     m = struct.pack('=IIBB', 1, stream_id, MSG_CONNECT_RESPONSE, result)
96     drv.sendall(m)
97
98 def send_data(stream_id, data):
99     m = struct.pack('=IIB', len(data), stream_id, MSG_DATA) + data
100     drv.sendall(m)
101
102 def send_eos(stream_id):
103     m = struct.pack('=IIB', 0, stream_id, MSG_EOS)
104     drv.sendall(m)
105
106 def send_reset(stream_id):
107     m = struct.pack('=IIB', 0, stream_id, MSG_RESET)
108     drv.sendall(m)
109
110 ACTION_NIL              = 0
111 ACTION_GET_BLOCK        = 2
112 ACTION_SET_MAP          = 4
113 ACTION_DIE              = 5
114 ACTION_EVENT            = 6
115 ACTION_CURRENT_VOLUME   = 7
116 ACTION_LOCATE_OBJECT    = 8
117 ACTION_RENAME_DISK      = 9
118 ACTION_WRITE            = ord('W')
119 ACTION_READ             = ord('R')
120 ACTION_FREE_LOCK        = 15
121 ACTION_DELETE_OBJECT    = 16
122 ACTION_RENAME_OBJECT    = 17
123 ACTION_MORE_CACHE       = 18
124 ACTION_COPY_DIR         = 19
125 ACTION_WAIT_CHAR        = 20
126 ACTION_SET_PROTECT      = 21
127 ACTION_CREATE_DIR       = 22
128 ACTION_EXAMINE_OBJECT   = 23
129 ACTION_EXAMINE_NEXT     = 24
130 ACTION_DISK_INFO        = 25
131 ACTION_INFO             = 26
132 ACTION_FLUSH            = 27
133 ACTION_SET_COMMENT      = 28
134 ACTION_PARENT           = 29
135 ACTION_TIMER            = 30
136 ACTION_INHIBIT          = 31
137 ACTION_DISK_TYPE        = 32
138 ACTION_DISK_CHANGE      = 33
139 ACTION_SET_DATE         = 34
140 ACTION_SAME_LOCK        = 40
141 ACTION_SCREEN_MODE      = 994
142 ACTION_READ_RETURN      = 1001
143 ACTION_WRITE_RETURN     = 1002
144 ACTION_FINDUPDATE       = 1004
145 ACTION_FINDINPUT        = 1005
146 ACTION_FINDOUTPUT       = 1006
147 ACTION_END              = 1007
148 ACTION_SEEK             = 1008
149 ACTION_TRUNCATE         = 1022
150 ACTION_WRITE_PROTECT    = 1023
151 ACTION_EXAMINE_FH       = 1034
152 ACTION_UNSUPPORTED      = 65535
153
154 ERROR_NO_FREE_STORE             = 103
155 ERROR_TASK_TABLE_FULL           = 105
156 ERROR_LINE_TOO_LONG             = 120
157 ERROR_FILE_NOT_OBJECT           = 121
158 ERROR_INVALID_RESIDENT_LIBRARY  = 122
159 ERROR_NO_DEFAULT_DIR            = 201
160 ERROR_OBJECT_IN_USE             = 202
161 ERROR_OBJECT_EXISTS             = 203
162 ERROR_DIR_NOT_FOUND             = 204
163 ERROR_OBJECT_NOT_FOUND          = 205
164 ERROR_BAD_STREAM_NAME           = 206
165 ERROR_OBJECT_TOO_LARGE          = 207
166 ERROR_ACTION_NOT_KNOWN          = 209
167 ERROR_INVALID_COMPONENT_NAME    = 210
168 ERROR_INVALID_LOCK              = 211
169 ERROR_OBJECT_WRONG_TYPE         = 212
170 ERROR_DISK_NOT_VALIDATED        = 213
171 ERROR_DISK_WRITE_PROTECTED      = 214
172 ERROR_RENAME_ACROSS_DEVICES     = 215
173 ERROR_DIRECTORY_NOT_EMPTY       = 216
174 ERROR_TOO_MANY_LEVELS           = 217
175 ERROR_DEVICE_NOT_MOUNTED        = 218
176 ERROR_SEEK_ERROR                = 219
177 ERROR_COMMENT_TOO_BIG           = 220
178 ERROR_DISK_FULL                 = 221
179 ERROR_DELETE_PROTECTED          = 222
180 ERROR_WRITE_PROTECTED           = 223
181 ERROR_READ_PROTECTED            = 224
182 ERROR_NOT_A_DOS_DISK            = 225
183 ERROR_NO_DISK                   = 226
184 ERROR_NO_MORE_ENTRIES           = 232
185
186 SHARED_LOCK         = -2
187 EXCLUSIVE_LOCK      = -1
188
189 LOCK_DIFFERENT      = -1
190 LOCK_SAME           = 0
191 LOCK_SAME_VOLUME    = 1
192
193 MODE_OLDFILE        = 1005  # Open existing file read/write positioned at beginning of file.
194 MODE_NEWFILE        = 1006  # Open freshly created file (delete old file) read/write, exclusive lock.
195 MODE_READWRITE      = 1004  # Open old file w/shared lock, creates file if doesn't exist.
196
197 OFFSET_BEGINNING    = -1    # Relative to Begining Of File.
198 OFFSET_CURRENT      = 0     # Relative to Current file position.
199 OFFSET_END          = 1     # relative to End Of File.
200
201 ST_ROOT             = 1
202 ST_USERDIR          = 2
203 ST_SOFTLINK         = 3     # looks like dir, but may point to a file!
204 ST_LINKDIR          = 4     # hard link to dir
205 ST_FILE             = -3    # must be negative for FIB!
206 ST_LINKFILE         = -4    # hard link to file
207 ST_PIPEFILE         = -5
208
209 current_stream_id = 0
210
211 class ObjectLock(object):
212     def __init__(self, key, mode, path):
213         self.key = key
214         self.mode = mode
215         self.path = path
216         self.entry_it = None
217
218 locks = {}
219
220 next_key = 1
221
222 def get_key():
223     global next_key
224     key = next_key
225     next_key = 1 if next_key == 0x7fffffff else (next_key + 1)
226     while key in locks:
227         key += 1
228     return key
229
230 def find_path(key, name):
231     i = name.find(':')
232     if i != -1:
233         vol = name[:i].lower()
234         if vol == '' or vol == 'pi0' or vol == 'pidisk':
235             key = 0
236         name = name[i + 1:]
237
238     if key == 0:
239         cp = ()
240     else:
241         cp = locks[key].path
242
243     while name:
244         i = name.find('/')
245         if i == -1:
246             comp = name
247             name = ''
248         else:
249             comp = name[:i]
250             name = name[i + 1:]
251
252         if len(comp) == 0:
253             if len(cp) == 0:
254                 return None
255             cp = cp[:-1]
256         else:
257             p = '.' if len(cp) == 0 else '/'.join(cp)
258             entries = os.listdir(p)
259             found = False
260             for e in entries:
261                 if comp.lower() == e.lower():
262                     cp = cp + (e,)
263                     found = True
264                     break
265             if not found:
266                 if len(name) == 0:
267                     cp = cp + (comp,)
268                 else:
269                     return None
270
271     return cp
272
273 def read_metadata(path):
274     protection = 0
275     comment = ''
276
277     if not os.path.isfile(path + METAFILE_EXTENSION):
278         return (protection, comment)
279
280     try:
281         f = open(path + METAFILE_EXTENSION, 'r')
282         for line in f:
283             if line[0] == 'p':
284                 try:
285                     protection = int(line[1:].strip())
286                 except ValueError:
287                     pass
288             elif line[0] == 'c':
289                 comment = line[1:].strip()[:79]
290         f.close()
291     except FileNotFoundError:
292         pass
293     return (protection, comment)
294
295 def write_metadata(path, protection=None, comment=None):
296     p, c = read_metadata(path)
297
298     if protection == None:
299         protection = p
300     if comment == None:
301         comment = c
302
303     if (p, c) == (protection, comment):
304         return True
305
306     try:
307         f = open(path + METAFILE_EXTENSION, 'w')
308         f.write('p' + str(protection) + '\n')
309         f.write('c' + comment + '\n')
310         f.close()
311     except FileNotFoundError as e:
312         logger.warning('Failed to write metadata for file %s: %s', path, e)
313         return False
314     return True
315
316 def process_locate_object(key, mode, name):
317     logger.debug('ACTION_LOCATE_OBJECT, key: %s, mode: %s, name: %s', key, mode, name)
318
319     cp = find_path(key, name)
320
321     if cp is None or not (len(cp) == 0 or os.path.exists('/'.join(cp))):
322         return struct.pack('>HH', 0, ERROR_OBJECT_NOT_FOUND)
323
324     # TODO: Must check if there is already a lock for this path,
325     # and if so, if the locks are compatible.
326
327     key = get_key()
328     locks[key] = ObjectLock(key, mode, cp)
329     return struct.pack('>HHI', 1, 0, key)
330
331 def process_free_lock(key):
332     logger.debug('ACTION_FREE_LOCK, key: %s', key)
333     if key in locks:
334         del locks[key]
335     return struct.pack('>HH', 1, 0)
336
337 def process_copy_dir(prev_key):
338     logger.debug('ACTION_COPY_DIR, prev_key: %s', prev_key)
339     ol = locks[prev_key]
340     key = get_key()
341     locks[key] = ObjectLock(key, ol.mode, ol.path)
342     return struct.pack('>HHI', 1, 0, key)
343
344 def process_parent(prev_key):
345     logger.debug('ACTION_PARENT, prev_key: %s', prev_key)
346     ol = locks[prev_key]
347     if len(ol.path) == 0:
348         key = 0
349     else:
350         key = get_key()
351         locks[key] = ObjectLock(key, SHARED_LOCK, ol.path[:-1])
352     return struct.pack('>HHI', 1, 0, key)
353
354 def mtime_to_dmt(mtime):
355     mtime = int(mtime)
356     days = mtime // 86400
357     left = mtime - days * 86400
358     mins = left // 60
359     secs = left - mins * 60
360     ticks = secs * 50
361     days -= 2922 # Days between 1970-01-01 and 1978-01-01
362     days = max(0, days) # If days are before Amiga epoc
363     return (days, mins, ticks)
364
365 def process_examine_object(key):
366     logger.debug('ACTION_EXAMINE_OBJECT, key: %s', key)
367     ol = locks[key]
368
369     if len(ol.path) == 0:
370         fn = 'PiDisk'
371         path = '.'
372     else:
373         fn = ol.path[-1]
374         path = '/'.join(ol.path)
375
376     days, mins, ticks = mtime_to_dmt(os.path.getmtime(path))
377     protection, comment = read_metadata(path)
378
379     if os.path.isfile(path):
380         size = os.path.getsize(path)
381         type_ = ST_FILE
382     else:
383         size = 0
384         type_ = ST_USERDIR
385         ol.entry_it = os.scandir(path)
386
387     size = min(size, 2 ** 31 - 1)
388     fn = (chr(len(fn)) + fn).encode('latin-1', 'ignore')
389     comment = (chr(len(comment)) + comment).encode('latin-1', 'ignore')
390     return struct.pack('>HHHhIIIII', 1, 0, 666, type_, size, protection, days, mins, ticks) + fn + comment
391
392 def process_examine_next(key, disk_key):
393     logger.debug('ACTION_EXAMINE_NEXT, key: %s, disk_key: %s', key, disk_key)
394     ol = locks[key]
395
396     if len(ol.path) == 0:
397         path = '.'
398     else:
399         path = '/'.join(ol.path)
400
401     if not os.path.isdir(path):
402         return struct.pack('>HH', 0, ERROR_OBJECT_WRONG_TYPE)
403
404     disk_key += 1
405
406     entry = next(ol.entry_it, None)
407     while entry and entry.name.endswith(METAFILE_EXTENSION):
408         entry = next(ol.entry_it, None)
409
410     if not entry:
411         return struct.pack('>HH', 0, ERROR_NO_MORE_ENTRIES)
412
413     fn = entry.name
414     path = ('/'.join(ol.path + (fn,)))
415
416     days, mins, ticks = mtime_to_dmt(entry.stat().st_mtime)
417     protection, comment = read_metadata(path)
418
419     if os.path.isfile(path):
420         size = os.path.getsize(path)
421         type_ = ST_FILE
422     else:
423         size = 0
424         type_ = ST_USERDIR
425
426     size = min(size, 2 ** 31 - 1)
427     fn = (chr(len(fn)) + fn).encode('latin-1', 'ignore')
428     comment = (chr(len(comment)) + comment).encode('latin-1', 'ignore')
429     return struct.pack('>HHHhIIIII', 1, 0, disk_key, type_, size, protection, days, mins, ticks) + fn + comment
430
431 def process_examine_fh(arg1):
432     logger.debug('ACTION_EXAMINE_FH, arg1: %s', arg1)
433
434     fn = open_file_handles[arg1].f.name
435     path = os.path.realpath(fn)
436     days, mins, ticks = mtime_to_dmt(os.path.getmtime(path))
437     protection, comment = read_metadata(path)
438
439     if os.path.isfile(path):
440         size = os.path.getsize(path)
441         type_ = ST_FILE
442     else:
443         size = 0
444         type_ = ST_USERDIR
445
446     size = min(size, 2 ** 31 - 1)
447     fn = (chr(len(fn)) + fn).encode('latin-1', 'ignore')
448     comment = (chr(len(comment)) + comment).encode('latin-1', 'ignore')
449     return struct.pack('>HHHhIIIII', 1, 0, 666, type_, size, protection, days, mins, ticks) + fn + comment
450
451
452 next_fp = 1
453
454 open_file_handles = {}
455
456 def get_file_ptr():
457     global next_fp
458     fp = next_fp
459     next_fp = 1 if next_fp == 0x7fffffff else next_fp + 1
460     while fp in open_file_handles:
461         fp += 1
462     return fp
463
464 class OpenFileHandle(object):
465     def __init__(self, fp, f, p):
466         self.fp = fp
467         self.f = f
468         self.p = p
469
470 def process_findxxx(mode, key, name):
471     if mode == ACTION_FINDINPUT:
472         logger.debug('ACTION_FINDINPUT, key: %s, name: %s', key, name)
473     elif mode == ACTION_FINDOUTPUT:
474         logger.debug('ACTION_FINDOUTPUT, key: %s, name: %s', key, name)
475     elif mode == ACTION_FINDUPDATE:
476         logger.debug('ACTION_FINDUPDATE, key: %s, name: %s', key, name)
477
478     cp = find_path(key, name)
479     if cp is None:
480         return struct.pack('>HH', 0, ERROR_OBJECT_NOT_FOUND)
481
482     path = '/'.join(cp)
483     if len(cp) == 0 or os.path.isdir(path):
484         return struct.pack('>HH', 0, ERROR_OBJECT_WRONG_TYPE)
485
486     # TODO: Must check if there already exists a non-compatible lock for this path.
487
488     # TODO: This must be handled better. Especially error reporting.
489
490     protection, _ = read_metadata(path)
491     try:
492         if mode == MODE_OLDFILE:
493             f = open(path, 'r+b')
494         elif mode == MODE_READWRITE:
495             f = open(path, 'r+b')
496             if protection & 16:
497                 protection = protection & 0b11101111
498                 write_metadata(path, protection=protection)
499         elif mode == MODE_NEWFILE:
500             if protection & 0x1:
501                 return struct.pack('>HH', 0, ERROR_DELETE_PROTECTED)
502             elif protection & 0x4:
503                 return struct.pack('>HH', 0, ERROR_WRITE_PROTECTED)
504             f = open(path, 'w+b')
505             if protection & 16:
506                 protection = protection & 0b11101111
507                 write_metadata(path, protection=protection)
508     except IOError:
509         if mode == MODE_READWRITE:
510             try:
511                 f = open(path, 'w+b')
512                 if protection & 16:
513                     protection = protection & 0b11101111
514                     write_metadata(path, protection=protection)
515             except IOError:
516                 return struct.pack('>HH', 0, ERROR_OBJECT_NOT_FOUND)
517         else:
518             return struct.pack('>HH', 0, ERROR_OBJECT_NOT_FOUND)
519
520     fp = get_file_ptr()
521     ofh = OpenFileHandle(fp, f, protection)
522     open_file_handles[fp] = ofh
523
524     return struct.pack('>HHI', 1, 0, fp)
525
526 def process_read(arg1, address, length):
527     logger.debug('ACTION_READ, arg1: %s, address: %s, length: %s', arg1, address, length)
528     protection = open_file_handles[arg1].p
529     if protection & 0x8:
530         return struct.pack('>HH', 0, ERROR_READ_PROTECTED)
531     f = open_file_handles[arg1].f
532     data = f.read(length)
533     if len(data) != 0:
534         write_mem(address, data)
535     return struct.pack('>HHI', 1, 0, len(data))
536
537 def process_write(arg1, address, length):
538     logger.debug('ACTION_WRITE, arg1: %s, address: %s, length: %s', arg1, address, length)
539     protection = open_file_handles[arg1].p
540     if protection & 0x4:
541         return struct.pack('>HH', 0, ERROR_WRITE_PROTECTED)
542     data = read_mem(address, length)
543     f = open_file_handles[arg1].f
544     f.seek(0, 1)
545     try:
546         f.write(data)
547     except IOError:
548         return struct.pack('>HH', 0, ERROR_DISK_FULL)
549     return struct.pack('>HHI', 1, 0, length)
550
551 def process_seek(arg1, new_pos, mode):
552     logger.debug('ACTION_SEEK, arg1: %s, new_pos: %s, mode: %s', arg1, new_pos, mode)
553
554     f = open_file_handles[arg1].f
555     old_pos = f.tell()
556
557     from_what = 0
558     if mode == OFFSET_CURRENT:
559         from_what = 1
560     elif mode == OFFSET_END:
561         from_what = 2
562
563     f.seek(new_pos, from_what)
564
565     return struct.pack('>HHi', 1, 0, old_pos)
566
567 def process_end(arg1):
568     logger.debug('ACTION_END, arg1: %s', arg1)
569
570     if arg1 in open_file_handles:
571         f = open_file_handles.pop(arg1).f
572         f.close()
573
574     return struct.pack('>HH', 1, 0)
575
576 def process_delete_object(key, name):
577     logger.debug('ACTION_DELETE_OBJECT, key: %s, name: %s', key, name)
578
579     cp = find_path(key, name)
580     if cp is None or len(cp) == 0:
581         return struct.pack('>HH', 0, ERROR_OBJECT_NOT_FOUND)
582
583     path = '/'.join(cp)
584     is_dir = os.path.isdir(path)
585
586     protection, _ = read_metadata(path)
587     if protection & 0x1:
588         return struct.pack('>HH', 0, ERROR_DELETE_PROTECTED)
589
590     try:
591         if is_dir:
592             os.rmdir(path)
593         else:
594             os.remove(path)
595         if os.path.isfile(path + METAFILE_EXTENSION):
596             os.remove(path + METAFILE_EXTENSION)
597     except:
598         if is_dir:
599             return struct.pack('>HH', 0, ERROR_DIRECTORY_NOT_EMPTY)
600         else:
601             return struct.pack('>HH', 0, ERROR_OBJECT_NOT_FOUND)
602
603     return struct.pack('>HH', 1, 0)
604
605 def process_rename_object(key, name, target_dir, new_name):
606     logger.debug('ACTION_RENAME_OBJECT, key: %s, name: %s, target_dir: %s, new_name: %s', key, name, target_dir, new_name)
607
608     cp1 = find_path(key, name)
609     if cp1 is None or len(cp1) == 0:
610         return struct.pack('>HH', 0, ERROR_OBJECT_NOT_FOUND)
611
612     from_path = '/'.join(cp1)
613     if not os.path.exists(from_path):
614         return struct.pack('>HH', 0, ERROR_OBJECT_NOT_FOUND)
615
616     cp2 = find_path(target_dir, new_name)
617     if cp2 is None or len(cp2) == 0:
618         return struct.pack('>HH', 0, ERROR_OBJECT_NOT_FOUND)
619
620     to_path = '/'.join(cp2)
621
622     if from_path == to_path:
623         return struct.pack('>HH', 1, 0)
624
625     if os.path.exists(to_path):
626         return struct.pack('>HH', 0, ERROR_OBJECT_EXISTS)
627
628     try:
629         os.rename(from_path, to_path)
630         if os.path.isfile(from_path + METAFILE_EXTENSION):
631             os.rename(from_path + METAFILE_EXTENSION, to_path + METAFILE_EXTENSION)
632     except:
633         return struct.pack('>HH', 0, ERROR_OBJECT_NOT_FOUND)
634
635     return struct.pack('>HH', 1, 0)
636
637 def process_create_dir(key, name):
638     logger.debug('ACTION_CREATE_DIR, key: %s, name: %s', key, name)
639
640     cp = find_path(key, name)
641     if cp is None or len(cp) == 0:
642         return struct.pack('>HH', 0, ERROR_OBJECT_NOT_FOUND)
643
644     try:
645         path = '/'.join(cp)
646         os.makedirs(path)
647     except:
648         return struct.pack('>HH', 0, ERROR_OBJECT_NOT_FOUND)
649
650     key = get_key()
651     locks[key] = ObjectLock(key, SHARED_LOCK, cp)
652     return struct.pack('>HHI', 1, 0, key)
653
654 def process_set_protect(key, name, mask):
655     logger.debug('ACTION_SET_PROTECT, key: %s, name: %s, mask: %s', key, name, mask)
656
657     cp = find_path(key, name)
658     if cp is None or len(cp) == 0:
659         return struct.pack('>HH', 0, ERROR_OBJECT_NOT_FOUND)
660
661     path = '/'.join(cp)
662     if write_metadata(path, protection=mask):
663         return struct.pack('>HH', 1, 0)
664     else:
665         return struct.pack('>HH', 0, ERROR_OBJECT_NOT_FOUND)
666
667 def process_set_comment(key, name, comment):
668     logger.debug('ACTION_SET_COMMENT, key: %s, name: %s, comment: %s', key, name, comment)
669
670     if len(comment) > 79:
671         return struct.pack('>HH', 0, ERROR_COMMENT_TOO_BIG)
672
673     cp = find_path(key, name)
674     if cp is None or len(cp) == 0:
675         return struct.pack('>HH', 0, ERROR_OBJECT_NOT_FOUND)
676
677     path = '/'.join(cp)
678     if write_metadata(path, comment=comment):
679         return struct.pack('>HH', 1, 0)
680     else:
681         return struct.pack('>HH', 0, ERROR_OBJECT_NOT_FOUND)
682
683 def process_same_lock(key1, key2):
684     logger.debug('ACTION_SAME_LOCK, key1: %s key2: %s', key1, key2)
685
686     if not (key1 in locks and key2 in locks):
687         return struct.pack('>HH', 0, LOCK_DIFFERENT)
688     elif locks[key1].path == locks[key2].path:
689         return struct.pack('>HH', 1, LOCK_SAME)
690     else:
691         return struct.pack('>HH', 0, LOCK_SAME_VOLUME)
692
693 def process_request(req):
694     #logger.debug('len(req): %s, req: %s', len(req), list(req))
695
696     (rtype,) = struct.unpack('>H', req[:2])
697
698     if rtype == ACTION_LOCATE_OBJECT:
699         key, mode, nlen = struct.unpack('>IHB', req[2:9])
700         name = req[9:9+nlen].decode('latin-1')
701         return process_locate_object(key, mode, name)
702     elif rtype == ACTION_FREE_LOCK:
703         (key,) = struct.unpack('>I', req[2:6])
704         return process_free_lock(key)
705     elif rtype == ACTION_COPY_DIR:
706         (key,) = struct.unpack('>I', req[2:6])
707         return process_copy_dir(key)
708     elif rtype == ACTION_PARENT:
709         (key,) = struct.unpack('>I', req[2:6])
710         return process_parent(key)
711     elif rtype == ACTION_EXAMINE_OBJECT:
712         (key,) = struct.unpack('>I', req[2:6])
713         return process_examine_object(key)
714     elif rtype == ACTION_EXAMINE_NEXT:
715         key, disk_key = struct.unpack('>IH', req[2:8])
716         return process_examine_next(key, disk_key)
717     elif rtype == ACTION_EXAMINE_FH:
718         (arg1,) = struct.unpack('>I', req[2:6])
719         return process_examine_fh(arg1)
720     elif rtype == ACTION_FINDINPUT or rtype == ACTION_FINDOUTPUT or rtype == ACTION_FINDUPDATE:
721         key, nlen = struct.unpack('>IB', req[2:7])
722         name = req[7:7+nlen].decode('latin-1')
723         return process_findxxx(rtype, key, name)
724     elif rtype == ACTION_READ:
725         arg1, address, length = struct.unpack('>III', req[2:14])
726         return process_read(arg1, address, length)
727     elif rtype == ACTION_WRITE:
728         arg1, address, length = struct.unpack('>III', req[2:14])
729         return process_write(arg1, address, length)
730     elif rtype == ACTION_SEEK:
731         arg1, new_pos, mode = struct.unpack('>Iii', req[2:14])
732         return process_seek(arg1, new_pos, mode)
733     elif rtype == ACTION_END:
734         (arg1,) = struct.unpack('>I', req[2:6])
735         return process_end(arg1)
736     elif rtype == ACTION_DELETE_OBJECT:
737         key, nlen = struct.unpack('>IB', req[2:7])
738         name = req[7:7+nlen].decode('latin-1')
739         return process_delete_object(key, name)
740     elif rtype == ACTION_RENAME_OBJECT:
741         key, target_dir, nlen, nnlen = struct.unpack('>IIBB', req[2:12])
742         name = req[12:12+nlen].decode('latin-1')
743         new_name = req[12+nlen:12+nlen+nnlen].decode('latin-1')
744         return process_rename_object(key, name, target_dir, new_name)
745     elif rtype == ACTION_CREATE_DIR:
746         key, nlen = struct.unpack('>IB', req[2:7])
747         name = req[7:7+nlen].decode('latin-1')
748         return process_create_dir(key, name)
749     elif rtype == ACTION_SET_PROTECT:
750         key, mask, nlen = struct.unpack('>IIB', req[2:11])
751         name = req[11:11+nlen].decode('latin-1')
752         return process_set_protect(key, name, mask)
753     elif rtype == ACTION_SET_COMMENT:
754         key, nlen, clen = struct.unpack('>IBB', req[2:8])
755         name = req[8:8+nlen].decode('latin-1')
756         comment = req[8+nlen:8+nlen+clen].decode('latin-1')
757         return process_set_comment(key, name, comment)
758     elif rtype == ACTION_SAME_LOCK:
759         key1, key2 = struct.unpack('>II', req[2:10])
760         return process_same_lock(key1, key2)
761     elif rtype == ACTION_UNSUPPORTED:
762         (dp_Type,) = struct.unpack('>H', req[2:4])
763         logger.warning('Unsupported action %d (Amiga/a314fs)', dp_Type)
764         return struct.pack('>HH', 0, ERROR_ACTION_NOT_KNOWN)
765     else:
766         logger.warning('Unsupported action %d (a314d/a314fs)', rtype)
767         return struct.pack('>HH', 0, ERROR_ACTION_NOT_KNOWN)
768
769 done = False
770
771 try:
772     idx = sys.argv.index('-ondemand')
773 except ValueError:
774     idx = -1
775
776 if idx != -1:
777     fd = int(sys.argv[idx + 1])
778     drv = socket.socket(fileno=fd)
779 else:
780     drv = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
781     drv.connect(('localhost', 7110))
782
783     send_register_req(b'a314fs')
784     stream_id, ptype, payload = wait_for_msg()
785     if payload[0] != 1:
786         logger.error('Unable to register service a314fs, shutting down')
787         drv.close()
788         done = True
789
790 if not done:
791     os.chdir(SHARED_DIRECTORY)
792     logger.info('a314fs is running, shared directory: %s', SHARED_DIRECTORY)
793
794 while not done:
795     stream_id, ptype, payload = wait_for_msg()
796
797     if ptype == MSG_CONNECT:
798         if payload == b'a314fs':
799             if current_stream_id is not None:
800                 send_reset(current_stream_id)
801             current_stream_id = stream_id
802             send_connect_response(stream_id, 0)
803         else:
804             send_connect_response(stream_id, 3)
805     elif ptype == MSG_DATA:
806         address, length = struct.unpack('>II', payload)
807         #logger.debug('address: %s, length: %s', address, length)
808         req = read_mem(address + 2, length - 2)
809         res = process_request(req)
810         write_mem(address + 2, res)
811         #write_mem(address, b'\xff\xff')
812         send_data(stream_id, b'\xff\xff')
813     elif ptype == MSG_EOS:
814         if stream_id == current_stream_id:
815             logger.debug('Got EOS, stream closed')
816             send_eos(stream_id)
817             current_stream_id = None
818     elif ptype == MSG_RESET:
819         if stream_id == current_stream_id:
820             logger.debug('Got RESET, stream closed')
821             current_stream_id = None