1from __future__ import unicode_literals
2
3from dvc.utils.compat import str, open
4
5import os
6
7
8class System(object):
9    @staticmethod
10    def is_unix():
11        return os.name != "nt"
12
13    @staticmethod
14    def hardlink(source, link_name):
15        import ctypes
16        from dvc.exceptions import DvcException
17
18        if System.is_unix():
19            try:
20                os.link(source, link_name)
21                return
22            except Exception as exc:
23                raise DvcException("link", cause=exc)
24
25        CreateHardLink = ctypes.windll.kernel32.CreateHardLinkW
26        CreateHardLink.argtypes = [
27            ctypes.c_wchar_p,
28            ctypes.c_wchar_p,
29            ctypes.c_void_p,
30        ]
31        CreateHardLink.restype = ctypes.wintypes.BOOL
32
33        res = CreateHardLink(link_name, source, None)
34        if res == 0:
35            raise DvcException("CreateHardLinkW", cause=ctypes.WinError())
36
37    @staticmethod
38    def symlink(source, link_name):
39        import ctypes
40        from dvc.exceptions import DvcException
41
42        if System.is_unix():
43            try:
44                os.symlink(source, link_name)
45                return
46            except Exception as exc:
47                msg = "failed to symlink '{}' -> '{}': {}"
48                raise DvcException(msg.format(source, link_name, str(exc)))
49
50        flags = 0
51        if source is not None and os.path.isdir(source):
52            flags = 1
53
54        func = ctypes.windll.kernel32.CreateSymbolicLinkW
55        func.argtypes = (ctypes.c_wchar_p, ctypes.c_wchar_p, ctypes.c_uint32)
56        func.restype = ctypes.c_ubyte
57
58        if func(link_name, source, flags) == 0:
59            raise DvcException("CreateSymbolicLinkW", cause=ctypes.WinError())
60
61    @staticmethod
62    def _reflink_darwin(src, dst):
63        import ctypes
64
65        clib = ctypes.CDLL("libc.dylib")
66        if not hasattr(clib, "clonefile"):
67            return -1
68
69        clonefile = clib.clonefile
70        clonefile.argtypes = [ctypes.c_char_p, ctypes.c_char_p, ctypes.c_int]
71        clonefile.restype = ctypes.c_int
72
73        return clonefile(
74            ctypes.c_char_p(src.encode("utf-8")),
75            ctypes.c_char_p(dst.encode("utf-8")),
76            ctypes.c_int(0),
77        )
78
79    @staticmethod
80    def _reflink_windows(src, dst):
81        return -1
82
83    @staticmethod
84    def _reflink_linux(src, dst):
85        import os
86        import fcntl
87
88        FICLONE = 0x40049409
89
90        s = open(src, "r")
91        d = open(dst, "w+")
92
93        try:
94            ret = fcntl.ioctl(d.fileno(), FICLONE, s.fileno())
95        except IOError:
96            s.close()
97            d.close()
98            os.unlink(dst)
99            raise
100
101        s.close()
102        d.close()
103
104        if ret != 0:
105            os.unlink(dst)
106
107        return ret
108
109    @staticmethod
110    def reflink(source, link_name):
111        import platform
112        from dvc.exceptions import DvcException
113
114        system = platform.system()
115        try:
116            if system == "Windows":
117                ret = System._reflink_windows(source, link_name)
118            elif system == "Darwin":
119                ret = System._reflink_darwin(source, link_name)
120            elif system == "Linux":
121                ret = System._reflink_linux(source, link_name)
122            else:
123                ret = -1
124        except IOError:
125            ret = -1
126
127        if ret != 0:
128            raise DvcException("reflink is not supported")
129
130    @staticmethod
131    def getdirinfo(path):
132        import ctypes
133        from ctypes import c_void_p, c_wchar_p, Structure, WinError, POINTER
134        from ctypes.wintypes import DWORD, HANDLE, BOOL
135
136        # NOTE: use this flag to open symlink itself and not the target
137        # See https://docs.microsoft.com/en-us/windows/desktop/api/
138        # fileapi/nf-fileapi-createfilew#symbolic-link-behavior
139        FILE_FLAG_OPEN_REPARSE_POINT = 0x00200000
140
141        FILE_FLAG_BACKUP_SEMANTICS = 0x02000000
142        FILE_SHARE_READ = 0x00000001
143        OPEN_EXISTING = 3
144
145        class FILETIME(Structure):
146            _fields_ = [("dwLowDateTime", DWORD), ("dwHighDateTime", DWORD)]
147
148        class BY_HANDLE_FILE_INFORMATION(Structure):
149            _fields_ = [
150                ("dwFileAttributes", DWORD),
151                ("ftCreationTime", FILETIME),
152                ("ftLastAccessTime", FILETIME),
153                ("ftLastWriteTime", FILETIME),
154                ("dwVolumeSerialNumber", DWORD),
155                ("nFileSizeHigh", DWORD),
156                ("nFileSizeLow", DWORD),
157                ("nNumberOfLinks", DWORD),
158                ("nFileIndexHigh", DWORD),
159                ("nFileIndexLow", DWORD),
160            ]
161
162        flags = FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OPEN_REPARSE_POINT
163
164        func = ctypes.windll.kernel32.CreateFileW
165        func.argtypes = [
166            c_wchar_p,
167            DWORD,
168            DWORD,
169            c_void_p,
170            DWORD,
171            DWORD,
172            HANDLE,
173        ]
174        func.restype = HANDLE
175
176        hfile = func(
177            path, 0, FILE_SHARE_READ, None, OPEN_EXISTING, flags, None
178        )
179        if hfile is None:
180            raise WinError()
181
182        func = ctypes.windll.kernel32.GetFileInformationByHandle
183        func.argtypes = [HANDLE, POINTER(BY_HANDLE_FILE_INFORMATION)]
184        func.restype = BOOL
185
186        info = BY_HANDLE_FILE_INFORMATION()
187        rv = func(hfile, info)
188
189        func = ctypes.windll.kernel32.CloseHandle
190        func.argtypes = [HANDLE]
191        func.restype = BOOL
192
193        func(hfile)
194
195        if rv == 0:
196            raise WinError()
197
198        return info
199
200    @staticmethod
201    def inode(path):
202        if System.is_unix():
203            import ctypes
204
205            inode = os.lstat(path).st_ino
206            # NOTE: See https://bugs.python.org/issue29619 and
207            # https://stackoverflow.com/questions/34643289/
208            # pythons-os-stat-is-returning-wrong-inode-value
209            inode = ctypes.c_ulong(inode).value
210        else:
211            # getdirinfo from ntfsutils works on both files and dirs
212            info = System.getdirinfo(path)
213            inode = abs(
214                hash(
215                    (
216                        info.dwVolumeSerialNumber,
217                        info.nFileIndexHigh,
218                        info.nFileIndexLow,
219                    )
220                )
221            )
222        assert inode >= 0
223        assert inode < 2 ** 64
224        return inode
225
226    @staticmethod
227    def _wait_for_input_windows(timeout):
228        import sys
229        import ctypes
230        import msvcrt
231        from ctypes.wintypes import DWORD, HANDLE
232
233        # https://docs.microsoft.com/en-us/windows/desktop/api/synchapi/nf-synchapi-waitforsingleobject
234        WAIT_OBJECT_0 = 0
235        WAIT_TIMEOUT = 0x00000102
236
237        func = ctypes.windll.kernel32.WaitForSingleObject
238        func.argtypes = [HANDLE, DWORD]
239        func.restype = DWORD
240
241        rc = func(msvcrt.get_osfhandle(sys.stdin.fileno()), timeout * 1000)
242        if rc not in [WAIT_OBJECT_0, WAIT_TIMEOUT]:
243            raise RuntimeError(rc)
244
245    @staticmethod
246    def _wait_for_input_posix(timeout):
247        import sys
248        import select
249
250        try:
251            select.select([sys.stdin], [], [], timeout)
252        except select.error:
253            pass
254
255    @staticmethod
256    def wait_for_input(timeout):
257        if System.is_unix():
258            return System._wait_for_input_posix(timeout)
259        else:
260            return System._wait_for_input_windows(timeout)
261
262    @staticmethod
263    def is_symlink(path):
264        if System.is_unix():
265            return os.path.islink(path)
266
267        # https://docs.microsoft.com/en-us/windows/desktop/fileio/
268        # file-attribute-constants
269        FILE_ATTRIBUTE_REPARSE_POINT = 0x400
270
271        if os.path.lexists(path):
272            info = System.getdirinfo(path)
273            return info.dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT
274        return False
275
276    @staticmethod
277    def is_hardlink(path):
278        if System.is_unix():
279            return os.stat(path).st_nlink > 1
280
281        info = System.getdirinfo(path)
282        return info.nNumberOfLinks > 1
283