Line data Source code
1 : /*
2 : * Copyright (C) 2024 Red Hat, Inc.
3 : * SPDX-License-Identifier: GPL-3.0-or-later
4 : */
5 :
6 : 'use strict';
7 :
8 : import type { JsonObject, JsonValue } from './_internal/common';
9 : import { Channel } from './channel';
10 : import { EventEmitter } from './event';
11 :
12 139 : function is_json_dict(value: JsonValue): value is JsonObject {
13 139 : return value?.constructor === Object;
14 139 : }
15 :
16 : /* RFC 7396 — JSON Merge Patch — functional */
17 139 : function json_merge(current: JsonValue, patch: JsonValue): JsonValue {
18 139 : if (is_json_dict(patch)) {
19 40 : const updated = is_json_dict(current) ? { ...current } : { };
20 :
21 139 : for (const [key, value] of Object.entries(patch)) {
22 40 : if (value === null) {
23 40 : delete updated[key];
24 40 : } else {
25 139 : updated[key] = json_merge(updated[key], value);
26 139 : }
27 139 : }
28 :
29 139 : return updated;
30 124 : } else {
31 124 : return patch;
32 124 : }
33 139 : }
34 :
35 : export interface FileInfo {
36 : type?: string;
37 : tag?: string;
38 : mode?: number;
39 : size?: number;
40 : uid?: number;
41 : user?: string | number;
42 : gid?: number;
43 : group?: string | number;
44 : mtime?: number;
45 : target?: string;
46 : ['r-ok']?: boolean,
47 : ['w-ok']?: boolean,
48 : ['x-ok']?: boolean,
49 : entries?: Record<string, FileInfo>;
50 : targets?: Record<string, FileInfo>;
51 : }
52 :
53 : export interface FsInfoError {
54 : problem: string;
55 : message: string;
56 : errno: string;
57 : }
58 :
59 : export interface FsInfoState {
60 : info?: FileInfo;
61 : error?: FsInfoError;
62 : loading?: true;
63 : }
64 :
65 : export interface FsInfoEvents {
66 : change(state: FsInfoState): void;
67 : close(message: JsonObject): void;
68 : }
69 :
70 405 : export class FsInfoClient extends EventEmitter<FsInfoEvents> {
71 141 : state: FsInfoState = { loading: true };
72 :
73 141 : private partial_state: JsonValue = null;
74 : private channel: Channel<string>;
75 :
76 141 : constructor(path: string, attrs: (keyof FileInfo)[], options?: JsonObject) {
77 141 : super();
78 :
79 141 : this.channel = new Channel({
80 141 : payload: "fsinfo",
81 141 : path,
82 141 : attrs,
83 141 : watch: true,
84 141 : ...options
85 141 : });
86 :
87 139 : this.channel.on('data', payload => {
88 139 : this.partial_state = json_merge(this.partial_state, JSON.parse(payload));
89 :
90 139 : if (is_json_dict(this.partial_state) && !this.partial_state.partial) {
91 139 : this.state = { ...this.partial_state };
92 139 : this.emit('change', this.state);
93 139 : }
94 139 : });
95 :
96 39 : this.channel.on('close', message => {
97 39 : this.emit('close', message);
98 39 : });
99 141 : }
100 :
101 8 : close() {
102 8 : this.channel.close();
103 8 : }
104 :
105 0 : static entry(info: FileInfo, name: string): FileInfo | null {
106 0 : return info.entries?.[name] ?? null;
107 0 : }
108 :
109 0 : static target(info: FileInfo, name: string): FileInfo | null {
110 0 : const entries = info.entries ?? {};
111 0 : const targets = info.targets ?? {};
112 :
113 0 : let entry = entries[name] ?? null;
114 0 : for (let i = 0; i < 40; i++) {
115 0 : const target = entry?.target;
116 0 : if (!target)
117 0 : return entry;
118 0 : if (target === '.')
119 0 : return info;
120 0 : entry = entries[target] ?? targets[target] ?? null;
121 0 : }
122 0 : return null; // ELOOP
123 0 : }
124 405 : }
125 :
126 20 : export function fsinfo(path: string, attrs: (keyof FileInfo)[], options?: JsonObject): Promise<FileInfo> {
127 20 : return new Promise((resolve, reject) => {
128 20 : const client = new FsInfoClient(path, attrs, { ...options, watch: false });
129 20 : client.on('close', (message) => {
130 3 : if (message.problem) {
131 3 : reject(message);
132 3 : } else if (client.state.info) {
133 20 : resolve(client.state.info);
134 4 : } else {
135 4 : reject(client.state.error);
136 4 : }
137 20 : });
138 20 : });
139 20 : }
|