import io import typing as ty from . import base from .. import multipart from .. import utils class Section(base.SectionBase): """ Functions used to manage files in IPFS's virtual “Mutable File System” (MFS) file storage space. """ @base.returns_no_item def cp(self, source, dest, **kwargs): """Copies files within the MFS. Due to the nature of IPFS this will not actually involve any of the file's content being copied. .. code-block:: python >>> client.files.ls("/") {'Entries': [ {'Size': 0, 'Hash': '', 'Name': 'Software', 'Type': 0}, {'Size': 0, 'Hash': '', 'Name': 'test', 'Type': 0} ]} >>> client.files.cp("/test", "/bla") >>> client.files.ls("/") {'Entries': [ {'Size': 0, 'Hash': '', 'Name': 'Software', 'Type': 0}, {'Size': 0, 'Hash': '', 'Name': 'bla', 'Type': 0}, {'Size': 0, 'Hash': '', 'Name': 'test', 'Type': 0} ]} Parameters ---------- source : str Filepath within the MFS to copy from dest : str Destination filepath within the MFS to which the file will be copied to """ args = (source, dest) return self._client.request('/files/cp', args, **kwargs) #TODO: Add `flush(path="/")` @base.returns_single_item(base.ResponseBase) def ls(self, path, **kwargs): """Lists contents of a directory in the MFS. .. code-block:: python >>> client.files.ls("/") {'Entries': [ {'Size': 0, 'Hash': '', 'Name': 'Software', 'Type': 0} ]} Parameters ---------- path : str Filepath within the MFS Returns ------- dict +---------+------------------------------------------+ | Entries | List of files in the given MFS directory | +---------+------------------------------------------+ """ args = (path,) return self._client.request('/files/ls', args, decoder='json', **kwargs) @base.returns_no_item def mkdir(self, path, parents=False, **kwargs): """Creates a directory within the MFS. .. code-block:: python >>> client.files.mkdir("/test") Parameters ---------- path : str Filepath within the MFS parents : bool Create parent directories as needed and do not raise an exception if the requested directory already exists """ kwargs.setdefault("opts", {})["parents"] = parents args = (path,) return self._client.request('/files/mkdir', args, **kwargs) @base.returns_no_item def mv(self, source, dest, **kwargs): """Moves files and directories within the MFS. .. code-block:: python >>> client.files.mv("/test/file", "/bla/file") Parameters ---------- source : str Existing filepath within the MFS dest : str Destination to which the file will be moved in the MFS """ args = (source, dest) return self._client.request('/files/mv', args, **kwargs) def read(self, path, offset=0, count=None, **kwargs): """Reads a file stored in the MFS. .. code-block:: python >>> client.files.read("/bla/file") b'hi' Parameters ---------- path : str Filepath within the MFS offset : int Byte offset at which to begin reading at count : int Maximum number of bytes to read Returns ------- bytes : MFS file contents """ opts = {"offset": offset} if count is not None: opts["count"] = count kwargs.setdefault("opts", {}).update(opts) args = (path,) return self._client.request('/files/read', args, **kwargs) @base.returns_no_item def rm(self, path, recursive=False, **kwargs): """Removes a file from the MFS. .. code-block:: python >>> client.files.rm("/bla/file") Parameters ---------- path : str Filepath within the MFS recursive : bool Recursively remove directories? """ kwargs.setdefault("opts", {})["recursive"] = recursive args = (path,) return self._client.request('/files/rm', args, **kwargs) @base.returns_single_item(base.ResponseBase) def stat(self, path, **kwargs): """Returns basic ``stat`` information for an MFS file (including its hash). .. code-block:: python >>> client.files.stat("/test") {'Hash': 'QmUNLLsPACCz1vLxQVkXqqLX5R1X345qqfHbsf67hvA3Nn', 'Size': 0, 'CumulativeSize': 4, 'Type': 'directory', 'Blocks': 0} Parameters ---------- path : str Filepath within the MFS Returns ------- dict : MFS file information """ args = (path,) return self._client.request('/files/stat', args, decoder='json', **kwargs) @base.returns_no_item def write(self, path, file, offset=0, create=False, truncate=False, count=None, **kwargs): """Writes to a mutable file in the MFS. .. code-block:: python >>> client.files.write("/test/file", io.BytesIO(b"hi"), create=True) Parameters ---------- path : str Filepath within the MFS file : Union[str, bytes, os.PathLike, io.RawIOBase, int] IO stream object with data that should be written offset : int Byte offset at which to begin writing at create : bool Create the file if it does not exist truncate : bool Truncate the file to size zero before writing count : int Maximum number of bytes to read from the source ``file`` """ opts = {"offset": offset, "create": create, "truncate": truncate} if count is not None: opts["count"] = count kwargs.setdefault("opts", {}).update(opts) args = (path,) body, headers = multipart.stream_files(file, chunk_size=self.chunk_size) return self._client.request('/files/write', args, data=body, headers=headers, **kwargs) class Base(base.ClientBase): files = base.SectionProperty(Section) def add(self, file: ty.Union[utils.path_t, int, io.IOBase], *files, recursive: bool = False, pattern: multipart.match_spec_t[ty.AnyStr] = None, trickle: bool = False, follow_symlinks: bool = False, period_special: bool = True, only_hash: bool = False, wrap_with_directory: bool = False, chunker: ty.Optional[str] = None, pin: bool = True, raw_leaves: bool = None, nocopy: bool = False, **kwargs): """Adds a file, several files or directory of files to IPFS Arguments marked as “directories only” will be ignored unless *file* refers to a directory path or file descriptor. Passing a directory file descriptor is currently restricted to Unix (due to Python standard library limitations on Windows) and will prevent the *nocopy* feature from working. .. code-block:: python >>> with io.open('nurseryrhyme.txt', 'w', encoding='utf-8') as f: ... numbytes = f.write('Mary had a little lamb') >>> client.add('nurseryrhyme.txt') {'Hash': 'QmZfF6C9j4VtoCsTp4KSrhYH47QMd3DNXVZBKaxJdhaPab', 'Name': 'nurseryrhyme.txt'} Directory uploads ----------------- By default only regular files and directories immediately below the given directory path/FD are uploaded to the connected IPFS node; to upload an entire directory tree instead, *recursive* can be set to ``True``. Symbolic links and special files (pipes, sockets, devices nodes, …) cannot be represented by the UnixFS data structure this call creates and hence are ignored while scanning the target directory, to include the targets of symbolic links in the upload set *follow_symlinks* to ``True``. The set of files and directories included in the upload may be restricted by passing any combination of glob matching strings, compiled regular expression objects and custom :class:`~ipfshttpclient.filescanner.Matcher` objects. A file or directory will be included if it matches of the patterns provided. For regular expressions please note that as predicting which directories are relevant to the given pattern is impossible to do reliably if *recursive* is set to ``True`` the entire directory hierarchy will always be scanned and compared to the given expression even if only very few files are actually matched by the expression. To avoid this, pass a custom matching class or use glob-patterns instead (which will only cause a scan of the directories required to match their value). Note that unlike the ``ipfs add`` CLI interface this implementation will be default include dot-files (“files that are hidden”) – any file or directory whose name starts with a period/dot character – in the upload. For behaviour that is similar to the CLI command set *pattern* to ``"**"`` – this enables the default glob behaviour of not matching dot-files unless *period_special* is set to ``False`` or the pattern actually starts with a period. Arguments --------- file A filepath, path-object, file descriptor or open file object the file or directory to add recursive Upload files in subdirectories, if *file* refers to a directory? pattern A `*glob* <https://docs.python.org/3/library/glob.html>`_ pattern, compiled regular expression object or arbitrary matcher used to limit the files and directories included as part of adding a directory (directories only) trickle Use trickle-dag format (optimized for streaming) when generating the dag; see `the FAQ <https://github.com/ipfs/faq/issues/218>` for more information follow_symlinks Follow symbolic links when recursively scanning directories? (directories only) period_special Treat files and directories with a leading period character (“dot-files”) specially in glob patterns? (directories only) If this is set these files will only be matched by path labels whose initial character is a period, but not by those starting with ``?``, ``*`` or ``[``. only_hash Only chunk and hash, but do not write to disk wrap_with_directory Wrap files with a directory object to preserve their filename chunker The chunking algorithm to use pin Pin this object when adding raw_leaves Use raw blocks for leaf nodes. (experimental). (Default: ``True`` when *nocopy* is True, or ``False`` otherwise) nocopy Add the file using filestore. Implies raw-leaves. (experimental). Returns ------- Union[dict, list] File name and hash of the added file node, will return a list of one or more items unless only a single file was given """ opts = { # type: ty.Dict[str, ty.Union[str, bool]] "trickle": trickle, "only-hash": only_hash, "wrap-with-directory": wrap_with_directory, "pin": pin, "raw-leaves": raw_leaves if raw_leaves is not None else nocopy, "nocopy": nocopy } if chunker is not None: opts["chunker"] = chunker kwargs.setdefault("opts", {}).update(opts) # There may be other cases where nocopy will silently fail to work, but # this is by far the most obvious one if isinstance(file, int) and nocopy: raise ValueError("Passing file descriptors is incompatible with *nocopy*") assert not isinstance(file, (tuple, list)), \ "Use `client.add(name1, name2, …)` to add several items" multiple = (len(files) > 0) to_send = ((file,) + files) if multiple else file body, headers, is_dir = multipart.stream_filesystem_node( to_send, chunk_size=self.chunk_size, follow_symlinks=follow_symlinks, period_special=period_special, patterns=pattern, recursive=recursive ) resp = self._client.request('/add', decoder='json', data=body, headers=headers, **kwargs) if not multiple and not is_dir and not wrap_with_directory: assert len(resp) == 1 return base.ResponseBase(resp[0]) elif kwargs.get("stream", False): return base.ResponseWrapIterator(resp, base.ResponseBase) return [base.ResponseBase(v) for v in resp] @base.returns_no_item def get(self, cid, target: utils.path_t = ".", **kwargs): """Downloads a file, or directory of files from IPFS Parameters ---------- cid : Union[str, cid.CIDv0, cid.CIDv1] The path to the IPFS object(s) to be outputted target The directory to place the downloaded files in Defaults to the current working directory. """ args = (str(cid),) return self._client.download('/get', target, args, **kwargs) def cat(self, cid, offset=0, length=-1, **kwargs): r"""Retrieves the contents of a file identified by hash. .. code-block:: python >>> client.cat('QmTkzDwWqPbnAh5YiV5VwcTLnGdwSNsNTn2aDxdXBFca7D') Traceback (most recent call last): ... ipfsapi.exceptions.Error: this dag node is a directory >>> client.cat('QmeKozNssnkJ4NcyRidYgDY2jfRZqVEoRGfipkgath71bX') b'<!DOCTYPE html>\n<html>\n\n<head>\n<title>ipfs example viewer</…' Parameters ---------- cid : Union[str, cid.CIDv0, cid.CIDv1] The name or path of the IPFS object(s) to be retrieved offset : int Byte offset to begin reading from length : int Maximum number of bytes to read(-1 for all) Returns ------- bytes The file's contents """ args = (str(cid),) opts = {} if offset != 0: opts['offset'] = offset if length != -1: opts['length'] = length kwargs.setdefault('opts', opts) return self._client.request('/cat', args, **kwargs) @base.returns_single_item(base.ResponseBase) def ls(self, cid, **kwargs): """Returns a list of objects linked to by the given hash. .. code-block:: python >>> client.ls('QmTkzDwWqPbnAh5YiV5VwcTLnGdwSNsNTn2aDxdXBFca7D') {'Objects': [ {'Hash': 'QmTkzDwWqPbnAh5YiV5VwcTLnGdwSNsNTn2aDxdXBFca7D', 'Links': [ {'Hash': 'Qmd2xkBfEwEs9oMTk77A6jrsgurpF3ugXSg7dtPNFkcNMV', 'Name': 'Makefile', 'Size': 174, 'Type': 2}, … {'Hash': 'QmSY8RfVntt3VdxWppv9w5hWgNrE31uctgTiYwKir8eXJY', 'Name': 'published-version', 'Size': 55, 'Type': 2} ] } ]} Parameters ---------- cid : Union[str, cid.CIDv0, cid.CIDv1] The path to the IPFS object(s) to list links from Returns ------- dict Directory information and contents """ args = (str(cid),) return self._client.request('/ls', args, decoder='json', **kwargs)