Last active
March 11, 2024 19:40
-
-
Save JamesTheAwesomeDude/54dbbf6293281d7f5c7965474b0995f4 to your computer and use it in GitHub Desktop.
Python get "user cache directory" on any OS
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# Usage: | |
# import _cachedir | |
# CACHEDIR = _cachedir.cachedir('my_application@company.com') | |
# CACHEDIR = _cachedir.cachedir('my_packagename@pypi.org') | |
# NOTE: | |
# This does not create a secure enclave of any kind. | |
# Obviously, malicious applications running on the same | |
# user account could read or modify the cache directory. | |
import os | |
from pathlib import Path | |
import platform | |
import re | |
import sys | |
import uuid | |
# https://hg.mozilla.org/releases/mozilla-release/file/652f653a58f0acdc1413e45ab35eae68a95cd1af/toolkit/mozapps/extensions/internal/XPIInstall.jsm#l178 | |
UNIQUE_REGEX = re.compile(r'^(\{[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\}|[a-z0-9-\._]*\@[a-z0-9-\._]+)$') | |
UMASK_PRIVDIR = 0o0700 | |
__all__ = ['cachedir'] | |
def cachedir(name): | |
if not UNIQUE_REGEX.match(name): | |
if '@' not in name and len(name) < 30: | |
raise ValueError(f'bad application ID. Did you mean {f"{name}@pypi.org"!r}?') | |
raise ValueError('bad application ID.') | |
app_dir = _base_usercachedir() / name | |
os.makedirs(app_dir, UMASK_PRIVDIR, exist_ok=True) | |
return app_dir | |
def _base_usercachedir(): | |
system = platform.system() | |
if system == 'Windows': | |
# https://learn.microsoft.com/en-us/windows/win32/shell/knownfolderid#FOLDERID_LOCALAPPDATA | |
return Path(_shell32_known_folder('F1B32785-6FBA-4FCF-9D55-7B8E7F157091')) | |
elif system == 'Darwin': | |
# FIXME https://developer.apple.com/documentation/foundation/filemanager/searchpathdirectory/cachesdirectory | |
return Path('~/Library/Caches').expanduser() | |
else: | |
# https://specifications.freedesktop.org/basedir-spec/latest/ar01s03.html | |
if not (system == 'Linux' or system.endswith('BSD') or system == 'AIX'): | |
warnings.warn(f'Unrecognized operating system, {system!r}') | |
if ['XDG_CACHE_HOME'] in os.environ: | |
return Path(os.environ['XDG_CACHE_HOME']) | |
else: | |
return Path('~/.cache').expanduser() | |
if platform.system() == 'Windows': | |
from contextlib import contextmanager | |
import ctypes | |
from ctypes import oledll | |
from ctypes import windll | |
import ctypes.wintypes | |
try: | |
wintypes_GUID = ctypes.wintypes.GUID | |
except AttributeError: | |
class wintypes_GUID(ctypes.Structure): | |
# https://learn.microsoft.com/en-us/windows/win32/api/guiddef/ns-guiddef-guid | |
_fields_ = [ | |
('Data1', ctypes.c_ulong), | |
('Data2', ctypes.c_ushort), | |
('Data3', ctypes.c_ushort), | |
('Data4', ctypes.c_ubyte * 8) | |
] | |
def __init__(self, guid=None): | |
super().__init__() # NOTE is this needed?? | |
if guid is not None: | |
# https://learn.microsoft.com/en-us/windows/win32/api/combaseapi/nf-combaseapi-clsidfromstring | |
if not isinstance(guid, uuid.UUID): | |
guid = uuid.UUID(guid) | |
clsid = f'{{{guid!s}}}' | |
errno = oledll.ole32.CLSIDFromString(clsid, ctypes.byref(self)) | |
if errno != 0: | |
raise RuntimeError(f'CLSIDFromString returned error code {errno}') | |
def _shell32_known_folder(guid, /, *, _flags=0, _handle=None): | |
folder_id = wintypes_GUID(guid) | |
result_ptr = ctypes.c_wchar_p() | |
with _freeing(oledll.ole32.CoTaskMemFree, result_ptr): | |
errno = windll.shell32.SHGetKnownFolderPath( | |
ctypes.byref(folder_id), | |
_flags, | |
_handle, | |
ctypes.byref(result_ptr) | |
) | |
if errno == 0: | |
return result_ptr.value | |
else: | |
raise RuntimeError(f'SHGetKnownFolderPath returned error code {errno}') | |
@contextmanager | |
def _freeing(freefunc, obj): | |
try: | |
yield obj | |
finally: | |
freefunc(obj) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment