Is there a way to create something like a symbolic link from a folder on a Windows machine to a directory on a Linux one?

Posted on

Problem :

I have a program that runs on a Windows VirtualBox VM, a and stores data in a specific folder. I would like to work up something like a symbolic link from this folder to a directory on a Linux server, but I haven’t been able to come up with a working solution.
I tried mapping the Linux folder structure to a drive letter and then making a Junction from the ‘original’ Windows folder to the one that’s mapped to the drive letter, but Windows won’t let me complete the link. I also tried connecting the Linux directory using SFTP and that didn’t work either.

Solution :

Here’s what I did.

First, I installed SAMBA on the Linux box and shared the drives I wanted to use. (This is a whole nother topic, but you can find plenty of descriptions on how to do it.)

Then, on the Windows box, I created the Python 3 class below to make the links.

It can be used also to create links to Windows shares.

But the short answer is – create shares on the linux box and use mklink in a command prompt to create a symbolic link.

Warning: this code is meant as a proof-of-concept. It’s a work in progress and is incomplete. I don’t claim it’s the best code, or even good code.

"""    
@Author: J. Michael Adams

Please cite the author's name if you use any of this code.

Creates symlinks for share folders

It places all the symlinks in a local folder. The test code defines that folder
as C:share_symlinks.

NOTE -
This program uses the "mklink" command of the Windows command shell.
That command normally requires elevated administrator privilege.
That requirement can be changed using the "secpol.msc" console under
Security Settings > Local Policies > User Rights Assignment > create symbolic links

It pipes in a list of shares from the "net view" command.

Each output line from net view has this format: share-name "Disk" other-info
If it does not contain " Disk ", then it does not have a share name.

We want to create a symlink for each share.
The links will be created in a particular directory.

You can specify a list of share names that will be excluded - that is, they will
not be linked to. A name can can contain wildcards.

Any share name that matches a name in the exclude list will be ignored.
"""

#TODO: create a config file: excludes, link_dir, remote_drive, notify always, email parms
#TODO: check the permission level
#TODO: check the value of comspec
#TODO: remove obsolete links - links that have the proper remote_drive
#TODO: create an email object for notification
#TODO: create an exception that emails a notification

import os
import subprocess

class LinkManager(object):

    def __init__(self, remote_drive):
        self.remote_drive = remote_drive

        self.share_paths = {}  # share_paths[share name] => full path to share
        self.new_links = {}
        self.all_links = {}
        self.bad_links = {}

    def get_shares(self, excludes=None):

        """ returns a dict: key = share name, value = full path to the share

        """

        import fnmatch

        if type(excludes) is not list:
            if excludes is None:
                excludes = []
            elif type(excludes) is str:
                excludes = [excludes]
            else:
                raise Exception

        # Always exclude shares that end with '$'. These are administrative shares in Windows.
        excludes.append('*$')
        # We want to ignore case when we compare strings in the excludes list.
        # So we'll convert all share names to lower case.
        excludes = [x.lower() for x in excludes]

        ## call net view for the share drive. This might produce "access denied".
        # http://stackoverflow.com/questions/3005437/windowserror-error-5-access-is-denied
        cmd = "net view {} /all".format(remote_drive)

        proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
                                shell=True, universal_newlines=True)
        (out, err) = proc.communicate()

        if err:
            return out, err  #TODO: should be an exception

        ## get the output of the command and parse the share names
        # we'll convert all lines to lower case.
        # Since this is Windows, file names are case insensitive.
        # We do this so we can compare to the lower case names in the exclude list.
        lines = out.lower().split('n')

        for line in lines:

            # search for " disk " surrounded by a space, in case "disk" is a share name.
            if " disk " not in line:
                continue

            share = line.split(' disk ')[0].strip()

            # Check the share name against the exclude list.
            # The list can have names with wildcards ('*' and '?'),
            # so we'll use fnmatch() to check it.
            found = False
            for exclude in excludes:
                if fnmatch.fnmatch(share, exclude):
                    found = True
                    break
            if found:
                continue

            self.share_paths[share] = os.path.join(remote_drive, share)

        return '', ''

    def make_links(self, link_dir):

        """
         link_dir is the full path to the directory that will contain the links
         self.share_paths is a dict: share-name => target

         returns 3 dicts:
                new_links: a dict of all newly created links,
                all_links: a dict of all links in the link directory
                bad_links: links that do not point to the share base.

                key = link (full path)
                value = target (full path)

                for bad_link: if the value is None, the link path is not a link
         a dict of created links:
        """

        result = []
        for share, path in self.share_paths.items():

            # Create a symlink to the link directory.

            link = os.path.join(link_dir, share)

            self.all_links[link] = path

            # If it's already a link, it should point to the proper place
            # If the link name exists, but it's not a link
            # it's an error (or at least an issue).
            if os.path.exists(link):
                if os.path.islink(link):
                    relative_target = os.readlink(link)
                    target = os.path.realpath(relative_target)
                    if target != path:
                        self.bad_links[link] = target
                else:
                    self.bad_links[link] = None
                continue

            proc = subprocess.Popen(["mklink", "/D", link, path],
                                    stdout=subprocess.PIPE, stderr=subprocess.PIPE,
                                    shell=True)
            (out, err) = proc.communicate()
            #TODO: check the output. err should be empty. out should not be.
            print("program output: ", out)
            print("program err: ", err)

            self.new_links[link] = path

            result.append((out, err))

        return

    def remove_obsolete(self):

        # for each link in the directory, see if the name is a share. If not, remove it.
        #TODO: code it.
        pass
    def remove_link(self, link):

        if os.path.islink(link):
            # This removes the link, not the target
            proc = subprocess.Popen(["rmdir", link], stdout=subprocess.PIPE,
                                    stderr=subprocess.PIPE, shell=True)
            (out, err) = proc.communicate()
        else:
            out = ''
            err = "{} is not a link".format(link)

        return out, err

    # send an email to server_notifications


############## TEST ############################

#############################
# define the shares that will not be linked to.
# The routine "get_shares() will add the entry '*$' to the
# exclude list.
#############################

excludes = ['trash', 'home', 'Users']


link_dir = r'C:share_symlinks'
remote_drive = r'\bilbao'

mgr = LinkManager(remote_drive)

mgr.get_shares(excludes)
mgr.make_links(link_dir)

testing = False
if testing:
    for link, full_share in mgr.all_links.items():

        sysout, syserr = mgr.remove_link(link)
        # both err and out should be blank

        print('rmdir out: {}'.format(sysout))
        print('rmdir err: {}'.format(syserr))

        continue

exit(0)

If you can’t change the folder in the program’s settings , then it’s probably nothing you can do, short of creating iSCSI target on your Linux PC (file target probably, to keep your partitions intact) and using iSCSI initiator on your Windows VM to connect to it (MS iSCSI initiator, StarWind iSCSI Initiator).

Alternatively, one can hex-edit program and point it to the mapped network drive, but that requires some skills.

UPDATE: I’ve found a solution, that looks promising and doesn’t require messing with iSCSI: Is there a way to map a UNC path to a local folder on Windows 2003?. It looks like mklink is able to map network shares (shame on me, I should’ve checked) and if it fails you can try Symbolic Link Driver for Windows XP.

Leave a Reply

Your email address will not be published.