| 1 | import os
|
|---|
| 2 | from io import BytesIO, StringIO, UnsupportedOperation
|
|---|
| 3 |
|
|---|
| 4 | from django.core.files.utils import FileProxyMixin
|
|---|
| 5 | from django.utils.functional import cached_property
|
|---|
| 6 |
|
|---|
| 7 |
|
|---|
| 8 | class File(FileProxyMixin):
|
|---|
| 9 | DEFAULT_CHUNK_SIZE = 64 * 2**10
|
|---|
| 10 |
|
|---|
| 11 | def __init__(self, file, name=None):
|
|---|
| 12 | self.file = file
|
|---|
| 13 | if name is None:
|
|---|
| 14 | name = getattr(file, "name", None)
|
|---|
| 15 | self.name = name
|
|---|
| 16 | if hasattr(file, "mode"):
|
|---|
| 17 | self.mode = file.mode
|
|---|
| 18 |
|
|---|
| 19 | def __str__(self):
|
|---|
| 20 | return self.name or ""
|
|---|
| 21 |
|
|---|
| 22 | def __repr__(self):
|
|---|
| 23 | return "<%s: %s>" % (self.__class__.__name__, self or "None")
|
|---|
| 24 |
|
|---|
| 25 | def __bool__(self):
|
|---|
| 26 | return bool(self.name)
|
|---|
| 27 |
|
|---|
| 28 | def __len__(self):
|
|---|
| 29 | return self.size
|
|---|
| 30 |
|
|---|
| 31 | @cached_property
|
|---|
| 32 | def size(self):
|
|---|
| 33 | if hasattr(self.file, "size"):
|
|---|
| 34 | return self.file.size
|
|---|
| 35 | if hasattr(self.file, "name"):
|
|---|
| 36 | try:
|
|---|
| 37 | return os.path.getsize(self.file.name)
|
|---|
| 38 | except (OSError, TypeError):
|
|---|
| 39 | pass
|
|---|
| 40 | if hasattr(self.file, "tell") and hasattr(self.file, "seek"):
|
|---|
| 41 | pos = self.file.tell()
|
|---|
| 42 | self.file.seek(0, os.SEEK_END)
|
|---|
| 43 | size = self.file.tell()
|
|---|
| 44 | self.file.seek(pos)
|
|---|
| 45 | return size
|
|---|
| 46 | raise AttributeError("Unable to determine the file's size.")
|
|---|
| 47 |
|
|---|
| 48 | def chunks(self, chunk_size=None):
|
|---|
| 49 | """
|
|---|
| 50 | Read the file and yield chunks of ``chunk_size`` bytes (defaults to
|
|---|
| 51 | ``File.DEFAULT_CHUNK_SIZE``).
|
|---|
| 52 | """
|
|---|
| 53 | chunk_size = chunk_size or self.DEFAULT_CHUNK_SIZE
|
|---|
| 54 | try:
|
|---|
| 55 | self.seek(0)
|
|---|
| 56 | except (AttributeError, UnsupportedOperation):
|
|---|
| 57 | pass
|
|---|
| 58 |
|
|---|
| 59 | while True:
|
|---|
| 60 | data = self.read(chunk_size)
|
|---|
| 61 | if not data:
|
|---|
| 62 | break
|
|---|
| 63 | yield data
|
|---|
| 64 |
|
|---|
| 65 | def multiple_chunks(self, chunk_size=None):
|
|---|
| 66 | """
|
|---|
| 67 | Return ``True`` if you can expect multiple chunks.
|
|---|
| 68 |
|
|---|
| 69 | NB: If a particular file representation is in memory, subclasses should
|
|---|
| 70 | always return ``False`` -- there's no good reason to read from memory in
|
|---|
| 71 | chunks.
|
|---|
| 72 | """
|
|---|
| 73 | return self.size > (chunk_size or self.DEFAULT_CHUNK_SIZE)
|
|---|
| 74 |
|
|---|
| 75 | def __iter__(self):
|
|---|
| 76 | # Iterate over this file-like object by newlines
|
|---|
| 77 | buffer_ = None
|
|---|
| 78 | for chunk in self.chunks():
|
|---|
| 79 | for line in chunk.splitlines(True):
|
|---|
| 80 | if buffer_:
|
|---|
| 81 | if endswith_cr(buffer_) and not equals_lf(line):
|
|---|
| 82 | # Line split after a \r newline; yield buffer_.
|
|---|
| 83 | yield buffer_
|
|---|
| 84 | # Continue with line.
|
|---|
| 85 | else:
|
|---|
| 86 | # Line either split without a newline (line
|
|---|
| 87 | # continues after buffer_) or with \r\n
|
|---|
| 88 | # newline (line == b'\n').
|
|---|
| 89 | line = buffer_ + line
|
|---|
| 90 | # buffer_ handled, clear it.
|
|---|
| 91 | buffer_ = None
|
|---|
| 92 |
|
|---|
| 93 | # If this is the end of a \n or \r\n line, yield.
|
|---|
| 94 | if endswith_lf(line):
|
|---|
| 95 | yield line
|
|---|
| 96 | else:
|
|---|
| 97 | buffer_ = line
|
|---|
| 98 |
|
|---|
| 99 | if buffer_ is not None:
|
|---|
| 100 | yield buffer_
|
|---|
| 101 |
|
|---|
| 102 | def __enter__(self):
|
|---|
| 103 | return self
|
|---|
| 104 |
|
|---|
| 105 | def __exit__(self, exc_type, exc_value, tb):
|
|---|
| 106 | self.close()
|
|---|
| 107 |
|
|---|
| 108 | def open(self, mode=None, *args, **kwargs):
|
|---|
| 109 | if not self.closed:
|
|---|
| 110 | self.seek(0)
|
|---|
| 111 | elif self.name and os.path.exists(self.name):
|
|---|
| 112 | self.file = open(self.name, mode or self.mode, *args, **kwargs)
|
|---|
| 113 | else:
|
|---|
| 114 | raise ValueError("The file cannot be reopened.")
|
|---|
| 115 | return self
|
|---|
| 116 |
|
|---|
| 117 | def close(self):
|
|---|
| 118 | self.file.close()
|
|---|
| 119 |
|
|---|
| 120 |
|
|---|
| 121 | class ContentFile(File):
|
|---|
| 122 | """
|
|---|
| 123 | A File-like object that takes just raw content, rather than an actual file.
|
|---|
| 124 | """
|
|---|
| 125 |
|
|---|
| 126 | def __init__(self, content, name=None):
|
|---|
| 127 | stream_class = StringIO if isinstance(content, str) else BytesIO
|
|---|
| 128 | super().__init__(stream_class(content), name=name)
|
|---|
| 129 | self.size = len(content)
|
|---|
| 130 |
|
|---|
| 131 | def __str__(self):
|
|---|
| 132 | return "Raw content"
|
|---|
| 133 |
|
|---|
| 134 | def __bool__(self):
|
|---|
| 135 | return True
|
|---|
| 136 |
|
|---|
| 137 | def open(self, mode=None):
|
|---|
| 138 | self.seek(0)
|
|---|
| 139 | return self
|
|---|
| 140 |
|
|---|
| 141 | def close(self):
|
|---|
| 142 | pass
|
|---|
| 143 |
|
|---|
| 144 | def write(self, data):
|
|---|
| 145 | self.__dict__.pop("size", None) # Clear the computed size.
|
|---|
| 146 | return self.file.write(data)
|
|---|
| 147 |
|
|---|
| 148 |
|
|---|
| 149 | def endswith_cr(line):
|
|---|
| 150 | """Return True if line (a text or bytestring) ends with '\r'."""
|
|---|
| 151 | return line.endswith("\r" if isinstance(line, str) else b"\r")
|
|---|
| 152 |
|
|---|
| 153 |
|
|---|
| 154 | def endswith_lf(line):
|
|---|
| 155 | """Return True if line (a text or bytestring) ends with '\n'."""
|
|---|
| 156 | return line.endswith("\n" if isinstance(line, str) else b"\n")
|
|---|
| 157 |
|
|---|
| 158 |
|
|---|
| 159 | def equals_lf(line):
|
|---|
| 160 | """Return True if line (a text or bytestring) equals '\n'."""
|
|---|
| 161 | return line == ("\n" if isinstance(line, str) else b"\n")
|
|---|