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
|
/**
* A group named in the address: `#/name@owner`, optionally followed by a path
* inside it — `#/name@owner/root/folder/file.jpg`.
*
* The same handle `GroupName` shows under every group, so a link reads the way
* the group is labelled. It is resolved against the signed-in account's own
* group list and nothing else: no hub route answers "which group is this
* name", so a name says nothing to someone who is not already a member, and
* nobody can probe for one. The UUID stays the group's identity; a renamed
* group breaks its name links, never its `#/group/<id>` ones.
*
* In the fragment, never the path: what follows `#` is not sent to the server,
* so a group's name and a file's path appear in no hub or proxy log and in no
* Referer.
*
* The owner is everything after the *last* `@`: a username cannot contain one
* (users.py, RegisterRequest), a group name can. Each segment is
* percent-decoded on its own, so a `/` inside a name or a file name travels as
* `%2F` without splitting the path.
*
* No imports, so `test_group_link.py` can execute the module as it is.
*/
function _decode(segment) {
try { return decodeURIComponent(segment); } catch { return null; }
}
// `@` is legal in a fragment and the owner is found by the last one, so the
// name's own need no escaping; everything else is escaped as a URI component.
function _encode(segment) {
return encodeURIComponent(segment).replace(/%40/g, '@');
}
/**
* `{ name, owner, path }` for a route (the hash without its `#`), or null when
* the route does not name a group. `path` is '' for the group itself.
*/
function parseGroupLink(route) {
if (!route || route[0] !== '/') return null;
const parts = route.slice(1).split('/');
const head = _decode(parts[0]);
if (!head) return null;
const at = head.lastIndexOf('@');
if (at <= 0 || at === head.length - 1) return null;
const rest = parts.slice(1).filter(Boolean).map(_decode);
if (rest.some((s) => s === null || s === '.' || s === '..')) return null;
return { name: head.slice(0, at), owner: head.slice(at + 1), path: rest.join('/') };
}
/** The route naming `group`, and `path` inside it when one is given. */
function groupLinkRoute(group, path = '') {
const head = `/${_encode(group.name)}@${_encode(group.owner_username || '')}`;
const tail = path ? '/' + path.split('/').filter(Boolean).map(_encode).join('/') : '';
return head + tail;
}
/**
* The group `link` names among `groups` (`/v1/groups/mine` rows), or null.
*
* Case-insensitively on the name, as the hub keeps it unique
* (`uq_groups_owner_name` is on `lower(name)`); the owner as typed first, then
* case-insensitively when that finds exactly one.
*/
function findLinkedGroup(groups, link) {
if (!link) return null;
const name = link.name.toLowerCase();
const named = (groups || []).filter((g) => (g.name || '').toLowerCase() === name);
const exact = named.find((g) => g.owner_username === link.owner);
if (exact) return exact;
const owner = link.owner.toLowerCase();
const loose = named.filter((g) => (g.owner_username || '').toLowerCase() === owner);
return loose.length === 1 ? loose[0] : null;
}
/**
* What `path` names in a group's index: `{ kind: 'file', entry }`,
* `{ kind: 'dir', dir }`, or null. An entry's `path` is its folder, root
* first, and `name` its file name — so a file is matched on both together.
* Folders come from the files under them and from the node's own listing,
* which is the only place an empty one appears.
*/
function resolveLinkedPath(entries, nodeDirs, path) {
if (!path) return null;
const cut = path.lastIndexOf('/');
const dir = cut < 0 ? '' : path.slice(0, cut);
const name = cut < 0 ? path : path.slice(cut + 1);
const entry = (entries || []).find((e) => (e.path || '') === dir && e.name === name);
if (entry) return { kind: 'file', entry };
const prefix = path + '/';
const isDir = (nodeDirs || []).includes(path)
|| (entries || []).some((e) => (e.path || '') === path || (e.path || '').startsWith(prefix));
return isDir ? { kind: 'dir', dir: path } : null;
}
export { parseGroupLink, groupLinkRoute, findLinkedGroup, resolveLinkedPath };
|