aboutsummaryrefslogtreecommitdiffstats
path: root/prototypes/connections/examples/select-text.py
blob: ffaf80bef0a80d36ef8dee114bb1471c06295ebd (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
#!/usr/bin/env python3
"""Save a UTF-8 document selection through selection.pure (character indices)."""
import argparse
import http.client
import json
from pathlib import Path
import re
import struct
from urllib.parse import urlsplit

LIMIT = (2 * 6 + 32) * 32768 + 8192


def main():
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument('file', type=Path)
    parser.add_argument('--key', required=True)
    parser.add_argument('--start', type=int, required=True, help='first Unicode code point, starting at 0')
    parser.add_argument('--end', type=int, required=True, help='exclusive Unicode code point boundary')
    parser.add_argument('--revision', type=int, default=0, help='0 creates; otherwise expected stored revision')
    parser.add_argument('--url', default='http://127.0.0.1:8767')
    args = parser.parse_args()
    url = urlsplit(args.url)
    try:
        port = url.port or 80
    except ValueError as error:
        parser.error(str(error))
    if (url.scheme != 'http' or url.hostname not in ['localhost', '127.0.0.1'] or
            url.username is not None or url.password is not None or url.path not in ['', '/'] or
            url.query or url.fragment):
        parser.error('use a local HTTP server origin')
    if not re.fullmatch(r'[A-Za-z0-9_-]{1,64}', args.key) or not 0 <= args.revision <= 2**63 - 1:
        parser.error('invalid object key or revision')
    try:
        with args.file.open('rb') as source:
            document = source.read(LIMIT - 8 + 1)
        if len(document) > LIMIT - 8:
            parser.error('document exceeds the prototype request limit')
        text = document.decode('utf-8')
    except (OSError, UnicodeError) as error:
        parser.error(str(error))
    if not 0 <= args.start <= args.end <= len(text):
        parser.error('selection is outside the document')
    start = len(text[:args.start].encode('utf-8'))
    selected = text[args.start:args.end].encode('utf-8')
    # The boundary layout is explicit; storage receives only selected bytes.
    payload = struct.pack('<II', start, len(selected)) + document
    headers = {'Content-Type': 'application/octet-stream'}
    headers['If-None-Match' if args.revision == 0 else 'If-Match'] = (
        '*' if args.revision == 0 else f'"{args.revision}"')
    connection = http.client.HTTPConnection(url.hostname, port, timeout=12)
    try:
        connection.request('PUT', '/objects/' + args.key, payload, headers)
        response = connection.getresponse()
        body = response.read(LIMIT + 1)
        if response.status != 200:
            parser.error(f'storage response {response.status}: {response.getheader("X-Pure-Error")}')
        if body != selected:
            parser.error('response differs from the selection; check the installed program (write may have occurred)')
        revision = response.getheader('ETag', '')
        if not re.fullmatch(r'"[1-9][0-9]*"', revision) or int(revision[1:-1]) > 2**63 - 1:
            parser.error('response has no valid stored revision (write may have occurred)')
    except (OSError, http.client.HTTPException) as error:
        parser.error(f'{error}; storage outcome is unknown if the request was sent')
    finally:
        connection.close()
    print(json.dumps({'revision': revision[1:-1], 'selected': selected.decode('utf-8'),
                      'stored_bytes': len(selected)}, ensure_ascii=False))


if __name__ == '__main__':
    main()