Skip to content

Commit

Permalink
Windows symlink: extract all system depended code to System static class
Browse files Browse the repository at this point in the history
  • Loading branch information
dmpetrov committed Apr 11, 2017
1 parent 8b024eb commit 4578b6a
Show file tree
Hide file tree
Showing 16 changed files with 147 additions and 137 deletions.
5 changes: 0 additions & 5 deletions dvc/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +0,0 @@
from dvc.runtime import Runtime


def setup():
Runtime.symlink_setup()
5 changes: 2 additions & 3 deletions dvc/command/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,7 @@

from dvc.config import ConfigError
from dvc.logger import Logger
from dvc.path.factory import PathFactory
from dvc.utils import cached_property
from dvc.system import System


class CmdBase(object):
Expand Down Expand Up @@ -89,7 +88,7 @@ def run(self):
pass

def get_cache_file_s3_name(self, cache_file):
cache_prefix_file_name = os.path.relpath(os.path.realpath(cache_file), os.path.realpath(self.git.git_dir))
cache_prefix_file_name = os.path.relpath(System.realpath(cache_file), System.realpath(self.git.git_dir))
file_name = os.path.relpath(cache_prefix_file_name, self.config.cache_dir)
dvc_file_path_trim = file_name.replace(os.sep, '/').strip('/')
return self.config.aws_storage_prefix + '/' + dvc_file_path_trim
Expand Down
5 changes: 3 additions & 2 deletions dvc/command/data_sync.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from dvc.logger import Logger
from dvc.exceptions import DvcException
from dvc.runtime import Runtime
from dvc.system import System


class DataSyncError(DvcException):
Expand Down Expand Up @@ -52,7 +53,7 @@ def define_args(self, parser):
pass

def run(self):
if os.path.islink(self.parsed_args.target):
if System.islink(self.parsed_args.target):
data_item = self.path_factory.existing_data_item(self.parsed_args.target)
return self.sync_symlink(data_item)

Expand All @@ -66,7 +67,7 @@ def sync_dir(self, dir):
fname = os.path.join(dir, f)
if os.path.isdir(fname):
self.sync_dir(fname)
elif os.path.islink(fname):
elif System.islink(fname):
self.sync_symlink(self.path_factory.existing_data_item(fname))
else:
raise DataSyncError('Unsupported file type "{}"'.format(fname))
Expand Down
28 changes: 4 additions & 24 deletions dvc/git_wrapper.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
import os
if os.name == 'nt':
from ctypes import create_unicode_buffer, windll

from dvc.logger import Logger
from dvc.config import Config
from dvc.executor import Executor, ExecutorError
from dvc.system import System


class GitWrapperI(object):
Expand All @@ -22,7 +21,7 @@ def lock_file(self):

@property
def git_dir_abs(self):
return os.path.realpath(self.git_dir)
return System.realpath(self.git_dir)

@property
def curr_dir_abs(self):
Expand Down Expand Up @@ -51,25 +50,6 @@ def git_path_to_system_path(path):
return path.replace('/', '\\')
return path

LONG_PATH_BUFFER_SIZE = 1024

@staticmethod
def get_long_path(path):
"""Convert short path to a full path. It is needed for Windows."""
if os.name != 'nt':
return path

buffer = create_unicode_buffer(GitWrapperI.LONG_PATH_BUFFER_SIZE)
get_long_path_name = windll.kernel32.GetLongPathNameW
result = get_long_path_name(unicode(path), buffer, GitWrapperI.LONG_PATH_BUFFER_SIZE)
if result == 0 or result > GitWrapperI.LONG_PATH_BUFFER_SIZE:
return path
return buffer.value

@staticmethod
def get_cwd():
return GitWrapperI.get_long_path(os.getcwd())

@staticmethod
def parse_porcelain_files(out):
result = []
Expand Down Expand Up @@ -161,11 +141,11 @@ def commit_all_changes_and_log_status(self, message):

@staticmethod
def abs_paths_to_relative(files):
cur_dir = os.path.realpath(os.curdir)
cur_dir = System.realpath(os.curdir)

result = []
for file in files:
result.append(os.path.relpath(os.path.realpath(file), cur_dir))
result.append(os.path.relpath(System.realpath(file), cur_dir))

return result

Expand Down
6 changes: 3 additions & 3 deletions dvc/path/data_item.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import os

import shutil

from dvc.path.path import Path
from dvc.exceptions import DvcException
from dvc.system import System
from dvc.utils import cached_property


Expand Down Expand Up @@ -100,12 +100,12 @@ def _symlink_file(self):
return os.path.relpath(self.cache.relative, data_file_dir)

def create_symlink(self):
os.symlink(self._symlink_file, self.data.relative)
System.symlink(self._symlink_file, self.data.relative)

def move_data_to_cache(self):
cache_dir = os.path.dirname(self.cache.relative)
if not os.path.isdir(cache_dir):
os.makedirs(cache_dir)

shutil.move(self.data.relative, self.cache.relative)
os.symlink(self._symlink_file, self.data.relative)
System.symlink(self._symlink_file, self.data.relative)
7 changes: 4 additions & 3 deletions dvc/path/factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,14 @@
from dvc.path.data_item import DataItem, DataItemError, NotInDataDirError
from dvc.path.path import Path
from dvc.path.stated_data_item import StatedDataItem
from dvc.system import System


class PathFactory(object):
def __init__(self, git, config):
self._git = git
self._config = config
self._curr_dir_abs = os.path.realpath(os.curdir)
self._curr_dir_abs = System.realpath(os.curdir)

def path(self, relative_raw):
return Path(relative_raw, self._git)
Expand All @@ -24,9 +25,9 @@ def existing_data_item(self, file):
if not os.path.exists(file):
raise DataItemError(u'Data file "%s" is not exist' % file)

if not os.path.islink(file):
if not System.islink(file):
raise DataItemError(u'Data file "%s" must be a symbolic link' % file)
resolved_symlink = os.path.realpath(file)
resolved_symlink = System.realpath(file)
return DataItem(file, self._git, self._config, resolved_symlink)

def to_data_items(self, files):
Expand Down
4 changes: 2 additions & 2 deletions dvc/path/path.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import os

from dvc.git_wrapper import GitWrapperI
from dvc.system import System


class Path(object):
def __init__(self, path, git):
if not os.path.isabs(path):
pwd = GitWrapperI.get_cwd()
pwd = System.get_cwd()
path = os.path.normpath(os.path.join(pwd, path))

self._abs = path
Expand Down
75 changes: 2 additions & 73 deletions dvc/runtime.py
Original file line number Diff line number Diff line change
@@ -1,96 +1,25 @@
import os
import re
import sys
import ctypes

from dvc.config import Config, ConfigI
from dvc.exceptions import DvcException
from dvc.executor import Executor
from dvc.git_wrapper import GitWrapper
from dvc.logger import Logger
from dvc.settings import Settings
from dvc.system import System


class Runtime(object):
CONFIG = 'dvc.conf'
SYMLINC_OVERRIDE = None
REALPATH_NATIVE = None

@staticmethod
def ls_command_name():
if os.name == 'nt':
return 'dir'
return 'ls'

@staticmethod
def symlink_setup():
if os.name != 'nt':
return

Runtime._setup_windows_symlink()

os.symlink = Runtime.symlink
os.path.islink = Runtime.is_link

Runtime.REALPATH_NATIVE = os.path.realpath
os.path.realpath = Runtime.realpath

@staticmethod
def _setup_windows_symlink():
func = ctypes.windll.kernel32.CreateSymbolicLinkW
func.argtypes = (ctypes.c_wchar_p, ctypes.c_wchar_p, ctypes.c_uint32)
func.restype = ctypes.c_ubyte
Runtime.SYMLINC_OVERRIDE = func

@staticmethod
def is_link(path):
if not os.path.exists(path):
return False

if not os.path.isfile(path):
return False

# It is definitely not the best way to check a symlink.
code, output, _ = Executor.exec_cmd(["dir", path], shell=True)
if code != 0:
return False

return '<SYMLINK>' in output

@staticmethod
def realpath(path):
if os.name != 'nt':
return Runtime.REALPATH_NATIVE(path)

if not os.path.islink(path):
return Runtime.REALPATH_NATIVE(path)

# It is definitely not the best way to check a symlink.
code, output, _ = Executor.exec_cmd(["dir", path], shell=True)
if code != 0 or not '<SYMLINK>' in output:
return os.path.realpath(path)

groups = re.compile(r'\[\S+\]$').findall(output.strip())
if len(groups) < 1:
return Runtime.REALPATH_NATIVE(path)

resolved_link = groups[0][1:-1]
return resolved_link

@staticmethod
def symlink(source, link_name):
'''symlink(source, link_name) - DVC override for Windows
Creates a symbolic link pointing to source named link_name'''

flags = 0
if source is not None and os.path.isdir(source):
flags = 1
if Runtime.SYMLINC_OVERRIDE(link_name, source, flags) == 0:
raise ctypes.WinError()

@staticmethod
def conf_file_path(git_dir):
return os.path.realpath(os.path.join(git_dir, Runtime.CONFIG))
return System.realpath(os.path.join(git_dir, Runtime.CONFIG))

@staticmethod
def run(cmd_class, parse_config=True):
Expand Down
4 changes: 2 additions & 2 deletions dvc/state_file.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import time

from dvc.exceptions import DvcException
from dvc.git_wrapper import GitWrapperI
from dvc.system import System


class StateFileError(DvcException):
Expand Down Expand Up @@ -127,7 +127,7 @@ def normalized_args(self):
return result

def get_dvc_path(self):
pwd = GitWrapperI.get_cwd()
pwd = System.get_cwd()
if not pwd.startswith(self.git.git_dir_abs):
raise StateFileError('the file cannot be created outside of a git repository')

Expand Down
101 changes: 101 additions & 0 deletions dvc/system.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import ctypes
import os
import re

if os.name == 'nt':
from ctypes import create_unicode_buffer, windll

from dvc.executor import Executor


class System(object):
SYMLINK_OUTPUT = '<SYMLINK>'
LONG_PATH_BUFFER_SIZE = 1024

@staticmethod
def is_unix():
return os.name != 'nt'

@staticmethod
def symlink(source, link_name):
if System.is_unix():
return os.symlink(source, link_name)

flags = 0
if source is not None and os.path.isdir(source):
flags = 1

func = ctypes.windll.kernel32.CreateSymbolicLinkW
func.argtypes = (ctypes.c_wchar_p, ctypes.c_wchar_p, ctypes.c_uint32)
func.restype = ctypes.c_ubyte

if func(link_name, source, flags) == 0:
raise ctypes.WinError()

@staticmethod
def islink(path):
# It is definitely not the best way to check a symlink.

if System.is_unix():
return os.path.islink(path)

if not os.path.exists(path):
return False

if not os.path.isfile(path):
return False

return System._get_symlink_string() is not None

@staticmethod
def _get_symlink_string(path):
code, output, _ = Executor.exec_cmd(["dir", path], shell=True)
if code != 0:
return False

lines = output.split('\n')
for line in lines:
if System.SYMLINK_OUTPUT in line:
return line
return None

@staticmethod
def realpath(path):
# It is definitely not the best way to check a symlink.

if System.is_unix():
return os.path.realpath(path)

output = System._get_symlink_string(path)
if output is None:
return os.path.realpath(path)

groups = re.compile(r'\[\S+\]$').findall(output.strip())
if len(groups) < 1:
return os.path.realpath(path)

resolved_link = groups[0][1:-1]
return resolved_link

@staticmethod
def ls_command_name():
if System.is_unix():
return 'ls'
return 'dir'

@staticmethod
def get_long_path(path):
"""Convert short path to a full path. It is needed for Windows."""
if System.is_unix():
return path

buffer = create_unicode_buffer(System.LONG_PATH_BUFFER_SIZE)
get_long_path_name = windll.kernel32.GetLongPathNameW
result = get_long_path_name(unicode(path), buffer, System.LONG_PATH_BUFFER_SIZE)
if result == 0 or result > System.LONG_PATH_BUFFER_SIZE:
return path
return buffer.value

@staticmethod
def get_cwd():
return System.get_long_path(os.getcwd())
Loading

0 comments on commit 4578b6a

Please sign in to comment.