PyRAMObjects 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- PyRAM.py +623 -0
- PyRAMErrors.py +68 -0
- pyramobjects-0.1.0.dist-info/METADATA +133 -0
- pyramobjects-0.1.0.dist-info/RECORD +7 -0
- pyramobjects-0.1.0.dist-info/WHEEL +5 -0
- pyramobjects-0.1.0.dist-info/licenses/LICENSE +20 -0
- pyramobjects-0.1.0.dist-info/top_level.txt +2 -0
PyRAM.py
ADDED
|
@@ -0,0 +1,623 @@
|
|
|
1
|
+
from PyRAMErrors import (
|
|
2
|
+
PyRAMError, ComponentNotInstalledError,
|
|
3
|
+
DiskAlreadyExistsError, DiskNotFoundError,
|
|
4
|
+
DiskCreationError, IncorrectSymbolInPath,
|
|
5
|
+
NoDiskInPath, FolderNotFile, FileNotFound,
|
|
6
|
+
PathNotFound, NotEnoughRAMError,
|
|
7
|
+
DiskNotAlreadyExistsError, FolderAlreadyExistsError,
|
|
8
|
+
FolderNotAlreadyExistsError, ProcessAlreadyExistsError,
|
|
9
|
+
ProcessNotAlreadyExistsError, WindowsDiskDeleteError,
|
|
10
|
+
ZeroOrNegativeRAMError, FailedToFormatDisk,
|
|
11
|
+
WindowsDiskFormatError, ForeignDiskError, InputError,
|
|
12
|
+
IncorrectDiskName
|
|
13
|
+
)
|
|
14
|
+
from typing import Any
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
import subprocess
|
|
17
|
+
import sys
|
|
18
|
+
import os
|
|
19
|
+
import platform
|
|
20
|
+
from warnings import deprecated
|
|
21
|
+
import warnings
|
|
22
|
+
from dataclasses import dataclass
|
|
23
|
+
import ctypes
|
|
24
|
+
import shutil
|
|
25
|
+
import re
|
|
26
|
+
|
|
27
|
+
MARKER = ".pyram_owned"
|
|
28
|
+
FILE_READ_ONLY_VOLUME = 0x00080000
|
|
29
|
+
IMDISK_UNIT_MIN, IMDISK_UNIT_MAX = 100, 199
|
|
30
|
+
|
|
31
|
+
if platform.system() != "Windows":
|
|
32
|
+
raise RuntimeError("Library only for Windows")
|
|
33
|
+
|
|
34
|
+
try:
|
|
35
|
+
import psutil
|
|
36
|
+
psutil_instaled = True
|
|
37
|
+
except(ImportError, ImportWarning):
|
|
38
|
+
psutil_instaled = False
|
|
39
|
+
|
|
40
|
+
try:
|
|
41
|
+
import winfspy
|
|
42
|
+
winfspy_installed = True
|
|
43
|
+
except(ImportError, ImportWarning):
|
|
44
|
+
warnings.warn("Warning: winfspy not installed!\nPls download winfspy on site https://winfsp.dev/ and download library winfspy.", RuntimeWarning)
|
|
45
|
+
winfspy_installed = False
|
|
46
|
+
|
|
47
|
+
def _find_imdisk() -> str:
|
|
48
|
+
exe = shutil.which("imdisk")
|
|
49
|
+
if exe:
|
|
50
|
+
return exe
|
|
51
|
+
for base in (os.environ.get("SystemRoot", r"C:\Windows"),):
|
|
52
|
+
for sub in ("System32", "Sysnative"):
|
|
53
|
+
p = os.path.join(base, sub, "imdisk.exe")
|
|
54
|
+
if os.path.isfile(p):
|
|
55
|
+
return p
|
|
56
|
+
raise ComponentNotInstalledError("ImDisk not found. Install ImDisk Toolkit: https://sourceforge.net/projects/imdisk-toolkit/")
|
|
57
|
+
|
|
58
|
+
class MemoryObject:
|
|
59
|
+
def __init__(self):
|
|
60
|
+
self.objects = {}
|
|
61
|
+
|
|
62
|
+
def write(self, path, value) -> None:
|
|
63
|
+
self.objects[path] = value
|
|
64
|
+
|
|
65
|
+
def read(self, path) -> Any:
|
|
66
|
+
try:
|
|
67
|
+
return self.objects[path]
|
|
68
|
+
except(KeyError):
|
|
69
|
+
raise KeyError(f"Object '{path}', not found!")
|
|
70
|
+
|
|
71
|
+
def delete(self, path) -> None:
|
|
72
|
+
del self.objects[path]
|
|
73
|
+
|
|
74
|
+
def object_sum(self, path1, path2) -> Any:
|
|
75
|
+
object1 = self.read(path=path1)
|
|
76
|
+
object2 = self.read(path=path2)
|
|
77
|
+
|
|
78
|
+
try:
|
|
79
|
+
return object1 + object2
|
|
80
|
+
except(TypeError):
|
|
81
|
+
raise TypeError(f"Can not concatenate '{object1}:{type(object1)}' and '{object2}:{type(object2)}'")
|
|
82
|
+
|
|
83
|
+
def object_sub(self, path1, path2) -> Any:
|
|
84
|
+
object1 = self.read(path=path1)
|
|
85
|
+
object2 = self.read(path=path2)
|
|
86
|
+
|
|
87
|
+
try:
|
|
88
|
+
return object1 - object2
|
|
89
|
+
except(TypeError):
|
|
90
|
+
raise TypeError(f"Can not subtract '{object1}:{type(object1)}' and '{object2}:{type(object2)}'")
|
|
91
|
+
|
|
92
|
+
def object_div(self, path1, path2) -> int | float | bool | complex:
|
|
93
|
+
object1 = self.read(path=path1)
|
|
94
|
+
object2 = self.read(path=path2)
|
|
95
|
+
|
|
96
|
+
try:
|
|
97
|
+
return object1 / object2
|
|
98
|
+
except(TypeError):
|
|
99
|
+
raise TypeError(f"Can not divide '{object1}:{type(object1)}' and '{object2}:{type(object2)}'")
|
|
100
|
+
|
|
101
|
+
def object_mul(self, path1, path2) -> Any:
|
|
102
|
+
object1 = self.read(path=path1)
|
|
103
|
+
object2 = self.read(path=path2)
|
|
104
|
+
|
|
105
|
+
try:
|
|
106
|
+
return object1 * object2
|
|
107
|
+
except(TypeError):
|
|
108
|
+
raise TypeError(f"Can not multiply '{object1}:{type(object1)}' and '{object2}:{type(object2)}'")
|
|
109
|
+
|
|
110
|
+
def object_equal(self, path1, path2) -> bool:
|
|
111
|
+
object1 = self.read(path=path1)
|
|
112
|
+
object2 = self.read(path=path2)
|
|
113
|
+
|
|
114
|
+
if object1 == object2:
|
|
115
|
+
return True
|
|
116
|
+
elif object1 != object2:
|
|
117
|
+
return False
|
|
118
|
+
else:
|
|
119
|
+
raise RuntimeError(f"Objects cannot be compared, {object1}:{type(object1)} and {object2}:{type(object2)}")
|
|
120
|
+
|
|
121
|
+
class MemoryPythonFolder:
|
|
122
|
+
def __init__(self):
|
|
123
|
+
self.filesystem = {}
|
|
124
|
+
self.default_disk = "C"
|
|
125
|
+
|
|
126
|
+
def write(self, path: str, value=None) -> None:
|
|
127
|
+
disk = path.split(":")[0]
|
|
128
|
+
|
|
129
|
+
if len(disk) > 1:
|
|
130
|
+
raise NoDiskInPath("Pls enter disk, for example 'C:/example/popato.txt'")
|
|
131
|
+
|
|
132
|
+
path_without_disk = path.split(":")[1]
|
|
133
|
+
blocked_symbol = {":", ";", ","}
|
|
134
|
+
clean_path = Path(path)
|
|
135
|
+
list_of_parts = clean_path.parts
|
|
136
|
+
# file_name = clean_path.name
|
|
137
|
+
|
|
138
|
+
if any(symbol in path_without_disk for symbol in blocked_symbol):
|
|
139
|
+
raise IncorrectSymbolInPath("Pls do not use ':', ';', ',' in path, for example 'C:/example/popato.txt'")
|
|
140
|
+
|
|
141
|
+
current = self.filesystem
|
|
142
|
+
|
|
143
|
+
for part in list_of_parts[:-1]:
|
|
144
|
+
if current == part:
|
|
145
|
+
current = current[part]
|
|
146
|
+
continue
|
|
147
|
+
current = current.setdefault(part, {})
|
|
148
|
+
|
|
149
|
+
if list_of_parts[-1] is not None and value is not None:
|
|
150
|
+
current[list_of_parts[-1]] = value
|
|
151
|
+
elif list_of_parts[-1] is not None and value is None:
|
|
152
|
+
current[list_of_parts[-1]] = {}
|
|
153
|
+
|
|
154
|
+
return self.filesystem
|
|
155
|
+
|
|
156
|
+
def read(self, path: str) -> Any:
|
|
157
|
+
disk = path.split(":")[0]
|
|
158
|
+
|
|
159
|
+
if len(disk) > 1:
|
|
160
|
+
raise NoDiskInPath("Pls enter disk, for example 'C:/example/popato.txt'")
|
|
161
|
+
|
|
162
|
+
path_without_disk = path.split(":")[1]
|
|
163
|
+
blocked_symbol = {":", ";", ","}
|
|
164
|
+
clean_path = Path(path)
|
|
165
|
+
list_of_parts = clean_path.parts
|
|
166
|
+
|
|
167
|
+
if any(symbol in path_without_disk for symbol in blocked_symbol):
|
|
168
|
+
raise IncorrectSymbolInPath("Pls do not use ':', ';', ',' in path, for example 'C:/example/popato.txt'")
|
|
169
|
+
|
|
170
|
+
current = self.filesystem
|
|
171
|
+
|
|
172
|
+
for part in list_of_parts:
|
|
173
|
+
try:
|
|
174
|
+
current = current[part]
|
|
175
|
+
except(KeyError):
|
|
176
|
+
raise FileNotFound(f"File {path} not found")
|
|
177
|
+
|
|
178
|
+
return current
|
|
179
|
+
|
|
180
|
+
def listdir(self, path: str) -> Any:
|
|
181
|
+
disk = path.split(":")[0]
|
|
182
|
+
|
|
183
|
+
if len(disk) > 1:
|
|
184
|
+
raise NoDiskInPath("Pls enter disk, for example 'C:/example/popato.txt'")
|
|
185
|
+
|
|
186
|
+
path_without_disk = path.split(":")[1]
|
|
187
|
+
blocked_symbol = {":", ";", ","}
|
|
188
|
+
clean_path = Path(path)
|
|
189
|
+
list_of_parts = clean_path.parts
|
|
190
|
+
|
|
191
|
+
if any(symbol in path_without_disk for symbol in blocked_symbol):
|
|
192
|
+
raise IncorrectSymbolInPath("Pls do not use ':', ';', ',' in path, for example 'C:/example/popato.txt'")
|
|
193
|
+
|
|
194
|
+
current = self.filesystem
|
|
195
|
+
|
|
196
|
+
for part in list_of_parts:
|
|
197
|
+
try:
|
|
198
|
+
current = current[part]
|
|
199
|
+
except(KeyError):
|
|
200
|
+
raise FileNotFound(f"File {path} not found")
|
|
201
|
+
|
|
202
|
+
if isinstance(current, str):
|
|
203
|
+
raise FolderNotFile("Path must end by folder, not file!")
|
|
204
|
+
|
|
205
|
+
return list(current.keys())
|
|
206
|
+
|
|
207
|
+
def delete(self, path: str) -> None:
|
|
208
|
+
disk = path.split(":")[0]
|
|
209
|
+
|
|
210
|
+
if len(disk) > 1:
|
|
211
|
+
raise NoDiskInPath("Pls enter disk, for example 'C:/example/popato.txt'")
|
|
212
|
+
|
|
213
|
+
path_without_disk = path.split(":")[1]
|
|
214
|
+
blocked_symbol = {":", ";", ","}
|
|
215
|
+
clean_path = Path(path)
|
|
216
|
+
list_of_parts = clean_path.parts
|
|
217
|
+
|
|
218
|
+
if any(symbol in path_without_disk for symbol in blocked_symbol):
|
|
219
|
+
raise IncorrectSymbolInPath("Pls do not use ':', ';', ',' in path, for example 'C:/example/popato.txt'")
|
|
220
|
+
|
|
221
|
+
current = self.filesystem
|
|
222
|
+
|
|
223
|
+
for part in list_of_parts[:-1]:
|
|
224
|
+
try:
|
|
225
|
+
current = current[part]
|
|
226
|
+
except KeyError:
|
|
227
|
+
raise PathNotFound(f"Path '{path}' not found")
|
|
228
|
+
|
|
229
|
+
try:
|
|
230
|
+
del current[list_of_parts[-1]]
|
|
231
|
+
except KeyError:
|
|
232
|
+
raise FileNotFound(f"File '{path}' not found")
|
|
233
|
+
|
|
234
|
+
class MemoryStorage:
|
|
235
|
+
def __init__(self, cleanup_on_exit: bool = False):
|
|
236
|
+
self.ImDiskPath = _find_imdisk()
|
|
237
|
+
self._cleanup_on_exit = cleanup_on_exit
|
|
238
|
+
self.disks = {}
|
|
239
|
+
self.folders = {}
|
|
240
|
+
self.process = {}
|
|
241
|
+
|
|
242
|
+
for d in self.get_all_drives():
|
|
243
|
+
drive = d.rstrip("\\")
|
|
244
|
+
self.disks[drive] = self._unit_by_letter(disk_name=drive)
|
|
245
|
+
|
|
246
|
+
def _is_owned(self, drive: str) -> bool:
|
|
247
|
+
unit = self._unit_by_letter(drive)
|
|
248
|
+
if unit is None:
|
|
249
|
+
return False
|
|
250
|
+
if not (IMDISK_UNIT_MIN <= unit <= IMDISK_UNIT_MAX):
|
|
251
|
+
return False
|
|
252
|
+
|
|
253
|
+
return True
|
|
254
|
+
|
|
255
|
+
def _is_admin(self) -> bool:
|
|
256
|
+
try:
|
|
257
|
+
return bool(ctypes.windll.shell32.IsUserAnAdmin())
|
|
258
|
+
except Exception:
|
|
259
|
+
return False
|
|
260
|
+
|
|
261
|
+
def check_disk_formatted(self, disk_name: str) -> bool:
|
|
262
|
+
if psutil_instaled == False:
|
|
263
|
+
raise ComponentNotInstalledError("Psutil not installed, please do it")
|
|
264
|
+
|
|
265
|
+
disk_name = self._normalizate_disk_name(disk_name)
|
|
266
|
+
|
|
267
|
+
partitions = psutil.disk_partitions(all=True)
|
|
268
|
+
|
|
269
|
+
for p in partitions:
|
|
270
|
+
if p.device.startswith(disk_name) or p.mountpoint.startswith(disk_name):
|
|
271
|
+
if p.fstype and p.fstype.upper() != 'RAW':
|
|
272
|
+
return True
|
|
273
|
+
else:
|
|
274
|
+
return False
|
|
275
|
+
|
|
276
|
+
return False
|
|
277
|
+
|
|
278
|
+
def _normalizate_disk_name(self, name: str) -> str:
|
|
279
|
+
if not isinstance(name, str):
|
|
280
|
+
raise InputError("Disk name must be string!")
|
|
281
|
+
|
|
282
|
+
name = name.upper()
|
|
283
|
+
|
|
284
|
+
if not name.endswith(":"):
|
|
285
|
+
name += ":"
|
|
286
|
+
|
|
287
|
+
return name
|
|
288
|
+
|
|
289
|
+
def _formatdr(self, disk_name: str) -> bool:
|
|
290
|
+
success = re.fullmatch(r"[A-Z]:?", disk_name) and len(disk_name) == 2
|
|
291
|
+
return bool(success)
|
|
292
|
+
|
|
293
|
+
def _imdisk(self, *args, timeout=60) -> subprocess.CompletedProcess[str]:
|
|
294
|
+
exe = self.ImDiskPath
|
|
295
|
+
r = subprocess.run([exe, *args], capture_output=True, text=True, timeout=timeout)
|
|
296
|
+
return r
|
|
297
|
+
|
|
298
|
+
def get_all_drives(self) -> list:
|
|
299
|
+
drives = os.listdrives()
|
|
300
|
+
return drives
|
|
301
|
+
|
|
302
|
+
def get_windows_disk(self) -> str:
|
|
303
|
+
return os.environ.get('SystemDrive')
|
|
304
|
+
|
|
305
|
+
def get_disk_info(self, disk_name: str) -> dict:
|
|
306
|
+
disk_name = self._normalizate_disk_name(name=disk_name)
|
|
307
|
+
|
|
308
|
+
owner = None
|
|
309
|
+
is_formatted = None
|
|
310
|
+
total_bytes = None
|
|
311
|
+
used_bytes = None
|
|
312
|
+
free_bytes = None
|
|
313
|
+
used_perstange = None
|
|
314
|
+
partitions = None
|
|
315
|
+
fs_type = None
|
|
316
|
+
mountpoint = None
|
|
317
|
+
opts = None
|
|
318
|
+
|
|
319
|
+
try:
|
|
320
|
+
disk_value = self.disks[disk_name]
|
|
321
|
+
except(KeyError):
|
|
322
|
+
raise DiskNotAlreadyExistsError(f"Disk: '{disk_name.rstrip(":")}' not already exists")
|
|
323
|
+
|
|
324
|
+
if self.get_windows_disk() == disk_name:
|
|
325
|
+
owner = "System"
|
|
326
|
+
elif isinstance(disk_value, subprocess.Popen) or self._is_owned(drive=disk_name):
|
|
327
|
+
owner = "PyRAM"
|
|
328
|
+
else:
|
|
329
|
+
owner = "Unknown"
|
|
330
|
+
|
|
331
|
+
is_formatted = self.check_disk_formatted(disk_name=disk_name)
|
|
332
|
+
|
|
333
|
+
if is_formatted and psutil_instaled:
|
|
334
|
+
try:
|
|
335
|
+
usage = psutil.disk_usage(disk_name)
|
|
336
|
+
|
|
337
|
+
total_bytes = usage.total
|
|
338
|
+
used_bytes = usage.used
|
|
339
|
+
free_bytes = usage.free
|
|
340
|
+
used_perstange = usage.percent
|
|
341
|
+
except:
|
|
342
|
+
pass
|
|
343
|
+
|
|
344
|
+
elif not psutil_instaled:
|
|
345
|
+
warnings.warn("Psutil not installed, some data may be unavailable")
|
|
346
|
+
|
|
347
|
+
if psutil_instaled:
|
|
348
|
+
partitions = psutil.disk_partitions(all=True)
|
|
349
|
+
|
|
350
|
+
for p in partitions:
|
|
351
|
+
if p.device.upper().rstrip(r"\\") == disk_name:
|
|
352
|
+
try:
|
|
353
|
+
fs_type = p.fstype
|
|
354
|
+
mountpoint = p.mountpoint
|
|
355
|
+
opts = p.opts
|
|
356
|
+
except:
|
|
357
|
+
pass
|
|
358
|
+
|
|
359
|
+
return DiskInfo(drive=disk_name, owner=owner,is_formatted=is_formatted, drive_opts=opts, fs=fs_type, label=self.get_drive_label(drive_letter=disk_name),
|
|
360
|
+
total_bytes=total_bytes, used_bytes=used_bytes, free_bytes=free_bytes, used_percent=used_perstange,
|
|
361
|
+
readonly=self.is_drive_readonly(drive_letter=disk_name), mountpoint=mountpoint)
|
|
362
|
+
|
|
363
|
+
def get_drive_label(self, drive_letter: str) -> str:
|
|
364
|
+
path = f"{drive_letter.rstrip('\\').rstrip(':')}:\\"
|
|
365
|
+
|
|
366
|
+
volume_name_buffer = ctypes.create_unicode_buffer(260)
|
|
367
|
+
|
|
368
|
+
success = ctypes.windll.kernel32.GetVolumeInformationW(
|
|
369
|
+
path,
|
|
370
|
+
volume_name_buffer,
|
|
371
|
+
len(volume_name_buffer),
|
|
372
|
+
None, None, None, None, 0
|
|
373
|
+
)
|
|
374
|
+
|
|
375
|
+
if success:
|
|
376
|
+
return volume_name_buffer.value or "Local disk"
|
|
377
|
+
else:
|
|
378
|
+
return "Unknown disk"
|
|
379
|
+
|
|
380
|
+
def is_drive_readonly(self, drive_letter: str) -> bool:
|
|
381
|
+
path = f"{drive_letter.rstrip('\\').rstrip(':')}:\\"
|
|
382
|
+
|
|
383
|
+
flags = ctypes.c_ulong(0)
|
|
384
|
+
|
|
385
|
+
success = ctypes.windll.kernel32.GetVolumeInformationW(
|
|
386
|
+
path, None, 0, None, None, ctypes.byref(flags), None, 0
|
|
387
|
+
)
|
|
388
|
+
|
|
389
|
+
if success:
|
|
390
|
+
return bool(flags.value & FILE_READ_ONLY_VOLUME)
|
|
391
|
+
else:
|
|
392
|
+
return None
|
|
393
|
+
|
|
394
|
+
def _imdisk_units(self) -> list[int]:
|
|
395
|
+
r = self._imdisk("-l")
|
|
396
|
+
return [int(n) for n in re.findall(r"ImDisk(\d+)", r.stdout)]
|
|
397
|
+
|
|
398
|
+
def _free_unit(self) -> int:
|
|
399
|
+
used = set(self._imdisk_units())
|
|
400
|
+
for u in range(IMDISK_UNIT_MIN, IMDISK_UNIT_MAX + 1):
|
|
401
|
+
if u not in used:
|
|
402
|
+
return u
|
|
403
|
+
raise PyRAMError("No free ImDisk units in PyRAM range")
|
|
404
|
+
|
|
405
|
+
def _unit_by_letter(self, disk_name: str) -> int | None:
|
|
406
|
+
target = self._normalizate_disk_name(disk_name)
|
|
407
|
+
for u in self._imdisk_units():
|
|
408
|
+
if self._letter_by_unit(u) == target:
|
|
409
|
+
return u
|
|
410
|
+
return None
|
|
411
|
+
|
|
412
|
+
def _letter_by_unit(self, unit: int) -> str | None:
|
|
413
|
+
r = self._imdisk("-l", "-u", str(unit))
|
|
414
|
+
m = re.search(r"^Drive letter:\s*([A-Za-z])\s*$", r.stdout, re.MULTILINE)
|
|
415
|
+
return m.group(1).upper() + ":" if m else None
|
|
416
|
+
|
|
417
|
+
def create_disk_imdisk(self, disk_name: str = "Z:", memory_mb_size: int = 2048, file_system: str = "ntfs") -> bool:
|
|
418
|
+
disk_name = self._normalizate_disk_name(disk_name)
|
|
419
|
+
|
|
420
|
+
if self._formatdr(disk_name=disk_name) == False:
|
|
421
|
+
raise IncorrectDiskName(f"Disk name not correct: '{disk_name}'")
|
|
422
|
+
|
|
423
|
+
if disk_name in self.disks:
|
|
424
|
+
raise DiskAlreadyExistsError(f"Disk: '{disk_name.rstrip(":")}' already exists")
|
|
425
|
+
|
|
426
|
+
if psutil_instaled == True:
|
|
427
|
+
bytes_val = psutil.virtual_memory().total
|
|
428
|
+
mb_val = bytes_val / (1024 ** 2)
|
|
429
|
+
|
|
430
|
+
if memory_mb_size > mb_val:
|
|
431
|
+
raise NotEnoughRAMError("You cannot specify more RAM than you actually have!")
|
|
432
|
+
|
|
433
|
+
elif psutil_instaled == False:
|
|
434
|
+
warnings.warn("Psutil not installed, can not check RAM restrictions", RuntimeWarning)
|
|
435
|
+
|
|
436
|
+
if memory_mb_size <= 0:
|
|
437
|
+
raise ZeroOrNegativeRAMError(f"RAM is zero or negative: '{memory_mb_size}'")
|
|
438
|
+
|
|
439
|
+
unit = self._free_unit()
|
|
440
|
+
r = self._imdisk(
|
|
441
|
+
"-a",
|
|
442
|
+
"-u", str(unit),
|
|
443
|
+
"-s", f"{memory_mb_size}M",
|
|
444
|
+
"-m", disk_name,
|
|
445
|
+
"-p", f"/fs:{file_system} /q /y /v:PyRAM",
|
|
446
|
+
)
|
|
447
|
+
|
|
448
|
+
if r.returncode != 0:
|
|
449
|
+
raise PyRAMError(f"imdisk failed ({r.returncode}): {r.stderr or r.stdout}")
|
|
450
|
+
|
|
451
|
+
self.disks[disk_name] = unit
|
|
452
|
+
|
|
453
|
+
if not self.format_imdisk(disk_name, file_system):
|
|
454
|
+
warnings.warn(f"Failed to format RAM disk '{disk_name.rstrip(":")}'\nYou need to run the process as an administrator.", RuntimeWarning)
|
|
455
|
+
|
|
456
|
+
return True
|
|
457
|
+
|
|
458
|
+
def remove_disk_imdisk(self, disk_name: str, force: bool = True) -> bool:
|
|
459
|
+
disk_name = self._normalizate_disk_name(disk_name)
|
|
460
|
+
|
|
461
|
+
if self._formatdr(disk_name=disk_name) == False:
|
|
462
|
+
raise IncorrectDiskName(f"Disk name not correct: '{disk_name}'")
|
|
463
|
+
|
|
464
|
+
sys_drive = self.get_windows_disk()
|
|
465
|
+
|
|
466
|
+
if sys_drive == disk_name:
|
|
467
|
+
raise WindowsDiskDeleteError("You can not delete main Windows disk")
|
|
468
|
+
|
|
469
|
+
unit = self._unit_by_letter(disk_name)
|
|
470
|
+
if unit is None:
|
|
471
|
+
raise DiskNotAlreadyExistsError(f"Disk '{disk_name.rstrip(":")}' not found in ImDisk")
|
|
472
|
+
if not (IMDISK_UNIT_MIN <= unit <= IMDISK_UNIT_MAX):
|
|
473
|
+
raise ForeignDiskError(f"Disk '{disk_name.rstrip(":")}' was not created by PyRAM")
|
|
474
|
+
|
|
475
|
+
r = self._imdisk("-D" if force else "-d", "-u", str(unit))
|
|
476
|
+
if r.returncode != 0:
|
|
477
|
+
raise PyRAMError(f"imdisk failed ({r.returncode}): {r.stderr or r.stdout}")
|
|
478
|
+
|
|
479
|
+
self.disks.pop(disk_name, None)
|
|
480
|
+
|
|
481
|
+
return True
|
|
482
|
+
|
|
483
|
+
def format_imdisk(self, disk_name: str, file_system: str = "NTFS") -> bool:
|
|
484
|
+
disk_name = self._normalizate_disk_name(disk_name)
|
|
485
|
+
|
|
486
|
+
if self._formatdr(disk_name=disk_name) == False:
|
|
487
|
+
raise IncorrectDiskName(f"Disk name not correct: '{disk_name}'")
|
|
488
|
+
|
|
489
|
+
if disk_name == self.get_windows_disk():
|
|
490
|
+
raise WindowsDiskFormatError("You can not format main Windows disk")
|
|
491
|
+
|
|
492
|
+
fs = file_system.upper()
|
|
493
|
+
if fs not in {"NTFS", "FAT32", "EXFAT"}:
|
|
494
|
+
raise ValueError("Unsupported file system")
|
|
495
|
+
|
|
496
|
+
unit = self._unit_by_letter(disk_name)
|
|
497
|
+
if unit is None:
|
|
498
|
+
raise DiskNotAlreadyExistsError(f"Disk {disk_name.rstrip(':')} not found in ImDisk")
|
|
499
|
+
if not (IMDISK_UNIT_MIN <= unit <= IMDISK_UNIT_MAX):
|
|
500
|
+
raise ForeignDiskError(f"Disk {disk_name.rstrip(':')} was not created by PyRAM")
|
|
501
|
+
|
|
502
|
+
r = subprocess.run(
|
|
503
|
+
["format.com", disk_name, f"/FS:{fs}", "/Q", "/Y", "/V:PyRAM"],
|
|
504
|
+
capture_output=True, text=True, timeout=60,
|
|
505
|
+
)
|
|
506
|
+
if r.returncode != 0:
|
|
507
|
+
self._imdisk("-D", "-u", str(unit))
|
|
508
|
+
raise PyRAMError(f"format failed ({r.returncode}): {r.stdout or r.stderr}")
|
|
509
|
+
|
|
510
|
+
return True
|
|
511
|
+
|
|
512
|
+
def get_all_disks(self) -> dict:
|
|
513
|
+
return self.disks
|
|
514
|
+
|
|
515
|
+
def get_all_folders(self) -> dict:
|
|
516
|
+
return self.folders
|
|
517
|
+
|
|
518
|
+
def get_all_processes(self) -> dict:
|
|
519
|
+
return self.process
|
|
520
|
+
|
|
521
|
+
@deprecated("Experimental method, not stable. Please use ramdisk methods")
|
|
522
|
+
def create_disk_winfspy(self, disk_name: str = "Z:") -> subprocess.Popen:
|
|
523
|
+
if winfspy_installed == False:
|
|
524
|
+
raise ComponentNotInstalledError("winfspy not installed!")
|
|
525
|
+
|
|
526
|
+
if disk_name in self.disks:
|
|
527
|
+
raise DiskAlreadyExistsError(f"Disk: '{disk_name.rstrip(":")}' already exists")
|
|
528
|
+
|
|
529
|
+
process = subprocess.Popen([
|
|
530
|
+
sys.executable,
|
|
531
|
+
"-m", "winfspy.memfs",
|
|
532
|
+
disk_name,
|
|
533
|
+
])
|
|
534
|
+
|
|
535
|
+
self.process[disk_name] = process
|
|
536
|
+
|
|
537
|
+
return process
|
|
538
|
+
|
|
539
|
+
@deprecated("Experimental method, not stable. Please use ramdisk methods")
|
|
540
|
+
def create_folder_winfspy(self, path: str) -> subprocess.Popen:
|
|
541
|
+
if winfspy_installed == False:
|
|
542
|
+
raise ComponentNotInstalledError("winfspy not installed!")
|
|
543
|
+
|
|
544
|
+
clead_path = Path(path)
|
|
545
|
+
|
|
546
|
+
if clead_path in self.folders:
|
|
547
|
+
raise FolderAlreadyExistsError(f"Folder: {path} already exists")
|
|
548
|
+
|
|
549
|
+
process = subprocess.Popen([
|
|
550
|
+
sys.executable,
|
|
551
|
+
"-m", "winfspy.memfs",
|
|
552
|
+
clead_path,
|
|
553
|
+
])
|
|
554
|
+
|
|
555
|
+
self.process[clead_path] = process
|
|
556
|
+
|
|
557
|
+
return process
|
|
558
|
+
|
|
559
|
+
@deprecated("Experimental method, not stable. Please use ramdisk methods")
|
|
560
|
+
def remove_storage_winfspy(self, pointer: str) -> bool:
|
|
561
|
+
if len(pointer) <= 2:
|
|
562
|
+
pointer = self._normalizate_disk_name(pointer)
|
|
563
|
+
|
|
564
|
+
if winfspy_installed == False:
|
|
565
|
+
raise ComponentNotInstalledError("winfspy not installed!")
|
|
566
|
+
|
|
567
|
+
if pointer not in self.disks and pointer not in self.folders:
|
|
568
|
+
raise ProcessNotAlreadyExistsError(f"Process: {pointer.rstrip(":")} not already exists")
|
|
569
|
+
|
|
570
|
+
process: subprocess.Popen = self.process.get(pointer)
|
|
571
|
+
|
|
572
|
+
if process is None:
|
|
573
|
+
return False
|
|
574
|
+
|
|
575
|
+
process.terminate()
|
|
576
|
+
try:
|
|
577
|
+
process.wait(timeout=5)
|
|
578
|
+
except subprocess.TimeoutExpired:
|
|
579
|
+
process.kill()
|
|
580
|
+
|
|
581
|
+
if len(pointer) >= 2:
|
|
582
|
+
del self.folders[pointer]
|
|
583
|
+
else:
|
|
584
|
+
del self.disks[pointer]
|
|
585
|
+
|
|
586
|
+
def _owned_disks(self) -> list[str]:
|
|
587
|
+
result = []
|
|
588
|
+
for u in self._imdisk_units():
|
|
589
|
+
if IMDISK_UNIT_MIN <= u <= IMDISK_UNIT_MAX:
|
|
590
|
+
letter = self._letter_by_unit(u)
|
|
591
|
+
if letter:
|
|
592
|
+
result.append(letter)
|
|
593
|
+
return result
|
|
594
|
+
|
|
595
|
+
def close(self, force: bool = False) -> None:
|
|
596
|
+
for drive in self._owned_disks():
|
|
597
|
+
try:
|
|
598
|
+
self.remove_disk_imdisk(drive, force=force)
|
|
599
|
+
except PyRAMError as e:
|
|
600
|
+
warnings.warn(f"Could not remove {drive}: {e}", RuntimeWarning)
|
|
601
|
+
|
|
602
|
+
def __enter__(self):
|
|
603
|
+
return self
|
|
604
|
+
|
|
605
|
+
def __exit__(self, exc_type, exc, tb):
|
|
606
|
+
if self._cleanup_on_exit:
|
|
607
|
+
self.close(force=True)
|
|
608
|
+
return False
|
|
609
|
+
|
|
610
|
+
@dataclass(frozen=True)
|
|
611
|
+
class DiskInfo:
|
|
612
|
+
drive: str
|
|
613
|
+
owner: str
|
|
614
|
+
is_formatted: bool
|
|
615
|
+
drive_opts: str | None
|
|
616
|
+
fs: str | None
|
|
617
|
+
label: str | None
|
|
618
|
+
total_bytes: int | None
|
|
619
|
+
used_bytes: int | None
|
|
620
|
+
free_bytes: int | None
|
|
621
|
+
used_percent: float | None
|
|
622
|
+
readonly: bool | None
|
|
623
|
+
mountpoint: str | None
|
PyRAMErrors.py
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
class PyRAMError(Exception):
|
|
2
|
+
"""A fundamental error in the PyRAM library."""
|
|
3
|
+
|
|
4
|
+
class ComponentNotInstalledError(PyRAMError):
|
|
5
|
+
"""A required component is not installed."""
|
|
6
|
+
|
|
7
|
+
class DiskAlreadyExistsError(PyRAMError):
|
|
8
|
+
"""Disk already exists."""
|
|
9
|
+
|
|
10
|
+
class DiskNotFoundError(PyRAMError):
|
|
11
|
+
"""Disk not found."""
|
|
12
|
+
|
|
13
|
+
class DiskCreationError(PyRAMError):
|
|
14
|
+
"""Failed to create disk."""
|
|
15
|
+
|
|
16
|
+
class IncorrectSymbolInPath(PyRAMError):
|
|
17
|
+
"""Character error in the path."""
|
|
18
|
+
|
|
19
|
+
class NoDiskInPath(PyRAMError):
|
|
20
|
+
"""No disk in the path."""
|
|
21
|
+
|
|
22
|
+
class FileNotFound(PyRAMError):
|
|
23
|
+
"""File not found."""
|
|
24
|
+
|
|
25
|
+
class FolderNotFile(PyRAMError):
|
|
26
|
+
"""A folder, not a file."""
|
|
27
|
+
|
|
28
|
+
class PathNotFound(PyRAMError):
|
|
29
|
+
"""Path not found."""
|
|
30
|
+
|
|
31
|
+
class NotEnoughRAMError(PyRAMError):
|
|
32
|
+
"""Not enough RAM."""
|
|
33
|
+
|
|
34
|
+
class DiskNotAlreadyExistsError(PyRAMError):
|
|
35
|
+
"""Disk does not exist."""
|
|
36
|
+
|
|
37
|
+
class FolderAlreadyExistsError(PyRAMError):
|
|
38
|
+
"""Folder already exists."""
|
|
39
|
+
|
|
40
|
+
class FolderNotAlreadyExistsError(PyRAMError):
|
|
41
|
+
"""Folder does not exist."""
|
|
42
|
+
|
|
43
|
+
class ProcessAlreadyExistsError(PyRAMError):
|
|
44
|
+
"""Process already exists."""
|
|
45
|
+
|
|
46
|
+
class ProcessNotAlreadyExistsError(PyRAMError):
|
|
47
|
+
"""Process does not exist."""
|
|
48
|
+
|
|
49
|
+
class WindowsDiskDeleteError(PyRAMError):
|
|
50
|
+
"""Windows disk deletion error"""
|
|
51
|
+
|
|
52
|
+
class WindowsDiskFormatError(PyRAMError):
|
|
53
|
+
"""Windows disk formattion error"""
|
|
54
|
+
|
|
55
|
+
class ZeroOrNegativeRAMError(PyRAMError):
|
|
56
|
+
"""Zero or negative RAM."""
|
|
57
|
+
|
|
58
|
+
class FailedToFormatDisk(PyRAMError):
|
|
59
|
+
"""Disk formatting error"""
|
|
60
|
+
|
|
61
|
+
class ForeignDiskError(PyRAMError):
|
|
62
|
+
"""Not PyRAM created"""
|
|
63
|
+
|
|
64
|
+
class InputError(PyRAMError):
|
|
65
|
+
"""incorrect answer"""
|
|
66
|
+
|
|
67
|
+
class IncorrectDiskName(PyRAMError):
|
|
68
|
+
"""Incorrect disk name"""
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: PyRAMObjects
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Safe Python API for ImDisk RAM disks on Windows plus in-memory objects
|
|
5
|
+
Author: lolololox
|
|
6
|
+
License: MIT
|
|
7
|
+
Keywords: ramdisk,imdisk,windows,in-memory
|
|
8
|
+
Classifier: Development Status :: 3 - Alpha
|
|
9
|
+
Classifier: Operating System :: Microsoft :: Windows
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
11
|
+
Requires-Python: >=3.13
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
License-File: LICENSE
|
|
14
|
+
Requires-Dist: psutil>=5.9
|
|
15
|
+
Dynamic: license-file
|
|
16
|
+
|
|
17
|
+
# PyRAM
|
|
18
|
+
|
|
19
|
+
A Python library for managing RAM disks on Windows. Based on ImDisk with RamDisk and WinFs with WinFsPy.
|
|
20
|
+
Project target it is:
|
|
21
|
+
|
|
22
|
+
1. Create more simple API than original library
|
|
23
|
+
2. Do more stable than original library
|
|
24
|
+
3. Give additional functional than original library
|
|
25
|
+
|
|
26
|
+
> [!CAUTION]
|
|
27
|
+
> **IMPORTANT:**
|
|
28
|
+
>
|
|
29
|
+
> 1. Early version of the library, bugs are possible
|
|
30
|
+
> 2. Disks may be removed with a delay
|
|
31
|
+
> 3. API can changes every update, please check actual documentation
|
|
32
|
+
> 4. Administrator privileges are required for the library to function properly
|
|
33
|
+
> 5. ONLY Windows
|
|
34
|
+
> 6. PyRAM only manages disks it created (ImDisk units 100-199) and refuses to touch others.
|
|
35
|
+
|
|
36
|
+
## Features
|
|
37
|
+
|
|
38
|
+
- Create RAM disks.
|
|
39
|
+
- Remove RAM disks.
|
|
40
|
+
- Track disks managed by a `MemoryStorage` instance.
|
|
41
|
+
- Simple API with minimal setup.
|
|
42
|
+
- Format RAM disks
|
|
43
|
+
- Virtual in-memory file system
|
|
44
|
+
|
|
45
|
+
## Requirements
|
|
46
|
+
|
|
47
|
+
- Windows
|
|
48
|
+
- Python
|
|
49
|
+
- ImDisk
|
|
50
|
+
- WinFs(Not strictly required, but necessary for the library to additional function)
|
|
51
|
+
- WinFsPy(Not strictly required, but necessary for the library to additional features)
|
|
52
|
+
- Python 3.12 and higer
|
|
53
|
+
|
|
54
|
+
RAM disk operations may require administrator privileges.
|
|
55
|
+
|
|
56
|
+
## Installation
|
|
57
|
+
|
|
58
|
+
Install ImDisk and the required Python dependencies before using PyRAM.
|
|
59
|
+
In short time meybe you can install PyRAM from PaPi(Pip install PyRAMObjects)
|
|
60
|
+
|
|
61
|
+
## Quick Start
|
|
62
|
+
|
|
63
|
+
```python
|
|
64
|
+
from PyRAM import MemoryStorage
|
|
65
|
+
|
|
66
|
+
storage = MemoryStorage()
|
|
67
|
+
|
|
68
|
+
# Create disk without parameters
|
|
69
|
+
storage.create_disk_ramdisk() # uses default parameters disk_name = "Z:", memory_mb_size = 2048, file_system = "ntfs"
|
|
70
|
+
|
|
71
|
+
# Create a 512 MB RAM disk
|
|
72
|
+
storage.create_disk_ramdisk(
|
|
73
|
+
disk_name="X:",
|
|
74
|
+
memory_mb_size=512,
|
|
75
|
+
file_system = "ntfs"
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
# Remove the RAM disk
|
|
79
|
+
storage.remove_disk_ramdisk(disk_name="X:")
|
|
80
|
+
|
|
81
|
+
with MemoryStorage(cleanup_on_exit=True) as storage1: # cleaning all PyRAM disks when app closing, becouse cleanup_on_exit=True
|
|
82
|
+
storage1.create_disk_imdisk(disk_name="K:", memory_mb_size=512)
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## Usage Notes
|
|
86
|
+
|
|
87
|
+
- RAM disks use volatile memory. Data stored on them should be considered temporary.
|
|
88
|
+
- A drive letter must be available when creating a RAM disk.
|
|
89
|
+
- Creating, formatting, or removing a RAM disk may require elevated permissions.
|
|
90
|
+
- The library's disk registry tracks disks managed by the current `MemoryStorage` instance.
|
|
91
|
+
|
|
92
|
+
## License
|
|
93
|
+
|
|
94
|
+
MIT
|
|
95
|
+
|
|
96
|
+
## Documentation
|
|
97
|
+
|
|
98
|
+
- classes
|
|
99
|
+
|
|
100
|
+
1. MemoryObject. Just dict with dicts
|
|
101
|
+
2. MemoryPythonFolder. Virtual file system, where you can save only string data types
|
|
102
|
+
3. MemoryStorage. Class where access create folders(experimental only WinFsPy) and disks in ram and see it in explorer
|
|
103
|
+
4. DiskInfo. Just dataclass, has no methods
|
|
104
|
+
|
|
105
|
+
### MemoryObject methods
|
|
106
|
+
|
|
107
|
+
1. write. Save value
|
|
108
|
+
2. read. Get value
|
|
109
|
+
3. delete. delete value
|
|
110
|
+
4. object_sum/object_sub/object_div/object_mul/object_equal. Operations with objects
|
|
111
|
+
|
|
112
|
+
### MemoryPythonFolder methods
|
|
113
|
+
|
|
114
|
+
1. write. Save value
|
|
115
|
+
2. read. Get value
|
|
116
|
+
3. listdir. Get list of folders
|
|
117
|
+
4. delete. Delete value
|
|
118
|
+
|
|
119
|
+
### MemoryStorage methods
|
|
120
|
+
|
|
121
|
+
1. get_all_drives. Get all drives
|
|
122
|
+
2. remove_disk_imdisk. Create disk in RAM using ImDisk
|
|
123
|
+
3. remove_disk_imdisk. Delete disk, that created using ImDisk
|
|
124
|
+
4. format_ramdisk. Format disk
|
|
125
|
+
5. get_all_disks/get_all_folders/get_all_processes. Get all disks/folders/processes
|
|
126
|
+
6. create_disk_winfspy. Create disk in RAM using WinFsPy
|
|
127
|
+
7. create_folder_winfspy. Create folder in RAM using WinFsPy
|
|
128
|
+
8. remove_storage_winfspy. Remove storage, that created using WinFsPy
|
|
129
|
+
9. get_windows_disk. Get Windows disk
|
|
130
|
+
10. check_disk_formatted. Check disk formatted or not
|
|
131
|
+
11. get_drive_label. Get disk label
|
|
132
|
+
12. is_drive_readonly. Check disk read-only or not
|
|
133
|
+
13. get_disk_info. Get info about disk
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
PyRAM.py,sha256=QffA21icHcDKqLhqNCPHbgI9H_pC5MRiusVvJbqUtvE,21720
|
|
2
|
+
PyRAMErrors.py,sha256=1mm3sr5El-pnpRJEelaqqDWeuLJAD8kVw_Hei6C6JMs,1745
|
|
3
|
+
pyramobjects-0.1.0.dist-info/licenses/LICENSE,sha256=NGSda9nG6kWtoYMffS-hwJx92ADfBIm2-__O30Vw3aw,1109
|
|
4
|
+
pyramobjects-0.1.0.dist-info/METADATA,sha256=ga2y4HJbIEkjDhuzUuFulLSuSkvFeOVR2Vh93Ux29XQ,4258
|
|
5
|
+
pyramobjects-0.1.0.dist-info/WHEEL,sha256=mOdXR9C3wMVov7cSY8PY-XCzo3SmmWCHvGQr5csJoao,91
|
|
6
|
+
pyramobjects-0.1.0.dist-info/top_level.txt,sha256=4MvBKbML7j0d0jNmPgd8tqR17pUbaHeVSXyitjyvzjY,18
|
|
7
|
+
pyramobjects-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
<!-- markdownlint-disable MD041 -->
|
|
2
|
+
Copyright (c) 2026 lolololox
|
|
3
|
+
|
|
4
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
5
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
6
|
+
in the Software without restriction, including without limitation the rights
|
|
7
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
8
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
9
|
+
furnished to do so, subject to the following conditions:
|
|
10
|
+
|
|
11
|
+
The above copyright notice and this permission notice shall be included in all
|
|
12
|
+
copies or substantial portions of the Software.
|
|
13
|
+
|
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
15
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
16
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
17
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
18
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
19
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
20
|
+
SOFTWARE.
|