CH-J SM Mobilespráva serverů přes SSH
Menu
Vydané zdroje

CH-J SM Mobile

Procházejte adresáře a soubory konkrétního vydání aplikace.

Stáhnout zdroje ZIP
CH-J / SM Mobile 175 souborů
sm-mobile-20261016T185235Z-8e93ecc1b59a Přidáno: · Sestaveno:
sources.dart Přidáno: Datum není evidováno 7,4 KB · 260 řádků
Kód Stáhnout soubor
1import 'dart:typed_data';
2import 'package:dartssh2/dartssh2.dart';
3import '../../core/failure.dart';
4import '../documents.dart';
5import '../sftp_service.dart';
7/// Size limits: text is edited in memory up to [maxEditableBytes]; viewers
8/// (images, PDF, archives, hex) read up to [maxViewBytes].
9const maxEditableBytes = 20 * 1024 * 1024;
10const maxViewBytes = 100 * 1024 * 1024;
12/// Where an open file comes from and how it is saved back.
13abstract class FileSource {
14 String get name;
16 /// Shown to the user: server and path, or the device location.
17 String get location;
19 /// 'remote', 'device' or 'new'.
20 String get kind;
22 /// A stable key for recent files and favorites.
23 String get key;
25 /// False for files without a place to save to (new, read-only).
26 bool get canSave;
28 /// Why saving is not possible, as a localization key.
29 String? get readOnlyReason;
31 Future<Uint8List> load({int maxBytes = maxEditableBytes});
33 /// Saves [bytes]. Throws 'remoteChanged' / 'documentChanged' when the
34 /// file changed since it was loaded, unless [force].
35 Future<void> save(
36 Uint8List bytes, {
37 bool force = false,
38 bool inPlace = false,
39 });
41 Map<String, dynamic> toJson();
44/// A file on the connected server, read and written over SFTP.
45class RemoteSource implements FileSource {
46 /// The current SFTP service; throws when the connection is gone.
47 final Future<SftpService> Function() service;
48 final String profileId, profileName, path;
49 SftpFileAttrs? baseline;
50 List<int>? baselineHash;
51 bool writable = true;
52 RemoteSource({
53 required this.service,
54 required this.profileId,
55 required this.profileName,
56 required this.path,
57 this.baseline,
58 });
60 @override
61 String get name => path.substring(path.lastIndexOf('/') + 1);
62 @override
63 String get location => '$profileName:$path';
64 @override
65 String get kind => 'remote';
66 @override
67 String get key => 'remote:$profileId:$path';
68 @override
69 bool get canSave => writable;
70 @override
71 String? get readOnlyReason => writable ? null : 'readOnlyRemote';
73 /// Permission bits for display, e.g. "rw-r--r--".
74 String? get permissions {
75 final mode = baseline?.mode?.value;
76 if (mode == null) return null;
77 const chars = 'rwxrwxrwx';
78 return [
79 for (var i = 0; i < 9; i++) mode & (1 << (8 - i)) != 0 ? chars[i] : '-',
80 ].join();
81 }
83 @override
84 Future<Uint8List> load({int maxBytes = maxEditableBytes}) async {
85 final s = await service();
86 final (bytes, attrs) = await s.readFile(path, maxBytes: maxBytes);
87 baseline = attrs;
88 baselineHash = await SftpService.sha256Of(bytes);
89 // Whether writing is allowed is decided by the server on save; a
90 // refusal is reported as missing permission.
91 return bytes;
92 }
94 @override
95 Future<void> save(
96 Uint8List bytes, {
97 bool force = false,
98 bool inPlace = false,
99 }) async {
100 final s = await service();
101 // Without a known state (a draft restored offline), an existing file is
102 // never overwritten silently.
103 if (baseline == null && !force && await s.statOrNull(path) != null) {
104 throw const Failure('remoteChanged');
105 }
106 final attrs = await s.saveFile(
107 path,
108 bytes,
109 expected: baseline,
110 expectedHash: baselineHash,
111 force: force,
112 inPlace: inPlace,
113 );
114 baseline = attrs;
115 baselineHash = await SftpService.sha256Of(bytes);
116 }
118 /// Whether the file on the server changed since it was loaded or saved.
119 Future<bool> changedOnServer() async {
120 final b = baseline;
121 if (b == null) return false;
122 try {
123 await (await service()).checkUnchanged(path, b, baselineHash);
124 return false;
125 } on Failure catch (e) {
126 if (e.code == 'remoteChanged' || e.code == 'remoteDeleted') return true;
127 rethrow;
128 }
129 }
131 @override
132 Map<String, dynamic> toJson() => {
133 'kind': kind,
134 'profileId': profileId,
135 'profileName': profileName,
136 'path': path,
137 if (baseline != null)
138 'baseline': {'size': baseline!.size, 'mtime': baseline!.modifyTime},
139 };
141 static RemoteSource fromJson(
142 Map<String, dynamic> m,
143 Future<SftpService> Function() service,
144 ) {
145 final b = m['baseline'] as Map?;
146 return RemoteSource(
147 service: service,
148 profileId: m['profileId'] as String,
149 profileName: m['profileName'] as String? ?? '',
150 path: m['path'] as String,
151 baseline: b == null
152 ? null
153 : SftpFileAttrs(
154 size: b['size'] as int?,
155 modifyTime: b['mtime'] as int?,
156 ),
157 );
158 }
161/// A file on the device, opened through the system document picker.
162class DeviceSource implements FileSource {
163 final DocumentPlatform documents;
164 DeviceDocument doc;
165 DateTime? _modified;
166 int? _size;
167 DeviceSource(this.documents, this.doc);
169 /// A document that was just written with [size] bytes.
170 DeviceSource.saved(this.documents, this.doc, int size)
171 : _modified = doc.modified,
172 _size = size;
174 @override
175 String get name => doc.name;
176 @override
177 String get location => doc.location ?? doc.name;
178 @override
179 String get kind => 'device';
180 @override
181 String get key => 'device:${doc.id}';
182 @override
183 bool get canSave => doc.writable;
184 @override
185 String? get readOnlyReason => doc.writable ? null : 'readOnlyDevice';
187 @override
188 Future<Uint8List> load({int maxBytes = maxEditableBytes}) async {
189 doc = await documents.stat(doc);
190 if ((doc.size ?? 0) > maxBytes) throw const Failure('fileTooLarge');
191 final bytes = await documents.read(doc);
192 if (bytes.length > maxBytes) throw const Failure('fileTooLarge');
193 _modified = doc.modified;
194 _size = bytes.length;
195 return bytes;
196 }
198 @override
199 Future<void> save(
200 Uint8List bytes, {
201 bool force = false,
202 bool inPlace = false,
203 }) async {
204 if (!force && _modified != null) {
205 final now = await documents.stat(doc);
206 if (now.modified != _modified || now.size != _size) {
207 throw const Failure('documentChanged');
208 }
209 }
210 doc = await documents.write(doc, bytes);
211 _modified = doc.modified;
212 _size = bytes.length;
213 }
215 @override
216 Map<String, dynamic> toJson() => {'kind': kind, 'doc': doc.toJson()};
218 static DeviceSource fromJson(Map<String, dynamic> m, DocumentPlatform d) =>
219 DeviceSource(d, DeviceDocument.fromMap(m['doc'] as Map));
222/// Rebuilds a source saved in a draft or the recent list.
223FileSource sourceFromJson(
224 Map<String, dynamic> m, {
225 required DocumentPlatform documents,
226 required Future<SftpService> Function(String profileId) sftpFor,
227 String fallbackName = 'untitled.txt',
228}) => switch (m['kind']) {
229 'remote' => RemoteSource.fromJson(m, () => sftpFor(m['profileId'] as String)),
230 'device' => DeviceSource.fromJson(m, documents),
231 _ => NewSource(m['name'] as String? ?? fallbackName),
232};
234/// A new file that has not been saved anywhere yet.
235class NewSource implements FileSource {
236 @override
237 final String name;
238 NewSource(this.name);
239 @override
240 String get location => name;
241 @override
242 String get kind => 'new';
243 @override
244 String get key => 'new:$name';
245 @override
246 bool get canSave => false;
247 @override
248 String? get readOnlyReason => null;
249 @override
250 Future<Uint8List> load({int maxBytes = maxEditableBytes}) async =>
251 Uint8List(0);
252 @override
253 Future<void> save(
254 Uint8List bytes, {
255 bool force = false,
256 bool inPlace = false,
257 }) async => throw const Failure('saveAsRequired');
258 @override
259 Map<String, dynamic> toJson() => {'kind': kind, 'name': name};
SHA-256e349f41188fac88f2f132ca14fdd6d25170c9ddeaddd28d1af2fadf0711acd1f

CH-J Proprietary Software License 1.14 · Zdroje jsou zveřejněny pod CH-J Proprietary Software License 1.14. Jejich dostupnost nemění licenční podmínky ani neposkytuje další oprávnění.

SHA-256 archivu46e427a43e87af091004f36145590bda8fe90086dfb0f631afeb5029fb0b5655
Soubory