summaryrefslogtreecommitdiff
path: root/lnmod.py
blob: a36593cb3abfc41231cadcefc91332762571e471 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
#!/usr/bin/env python
#This program is free software: you can redistribute it and/or modify
#it under the terms of the GNU General Public License as published by
#the Free Software Foundation, either version 3 of the License, or
#(at your option) any later version.
#
#This program is distributed in the hope that it will be useful,
#but WITHOUT ANY WARRANTY; without even the implied warranty of
#MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#GNU General Public License for more details.
#
#You should have received a copy of the GNU General Public License
#along with this program.  If not, see <http://www.gnu.org/licenses/>.
import os
import argparse
import signal
from collections import namedtuple


#Define a namedtuple to cleanly handle symbolic link data.
#Because we are performing potentially dangerous operations on the filesystem
#it is necessary to store data in a tuple. E.G. Write once, examine, execute.  
#... Can't afford to screw up.
Link = namedtuple('Link', ['path', 'destination'])


def readlinkabs(l):
    """Return an absolute path for the destination 
    of a symlink
    """
    if not os.path.islink(l):
        return ''
    p = os.readlink(l)
    if os.path.isabs(p):
        return p
    return os.path.abspath(os.path.join(os.path.dirname(l), p))

def generate_link_map(path):
    """Walk through "path" directory structure and record all symbolic links,
    and their destinations.
    
    return: list of Link namedtuples
    """
    original_paths = []
    link_paths = []
    links = []
    
    for root, dirnames, filenames in path:
        # For each directory
        for dirname in dirnames:
            dir_path = os.path.join(root, dirname)
            if readlinkabs(dir_path):
                original_paths.append(dir_path)
                link_paths.append(readlinkabs(dir_path))

            #For each file
            for filename in filenames:
                file_path = os.path.join(root, filename)
                if readlinkabs(file_path):
                    # The way os.walk nests filenames; ignore replicated entities
                    if file_path in original_paths or file_path in link_paths:
                        continue
                    #Store symbolic link paths
                    original_paths.append(file_path)
                    link_paths.append(readlinkabs(file_path))
    #Generate a list of Links
    for original_path, link_path in zip(original_paths, link_paths):
        links.append(Link(original_path, link_path))
    return links

def get_link_map(link_map):
    """return: yield Link namedtuples
    """
    for link in link_map:
        yield link

def generate_replacement_link_map(link_map, search, replace):
    """Find and replace occurrence of search parameter in link. 
    return: list of Link namedtuples
    """
    links = []
    for link in link_map:
        if search in link.destination:
            linkr = link.destination.replace(search, replace)
            if linkr:
                #This should never happen... but if it does, we're ready.
                if link.path == linkr:
                    print("Ignoring (dangerous) duplicated path: {}".format(link.path))
                    continue
                links.append(Link(link.path, linkr))
    return links

def replace_links(link_map):
    """Perform physical link replacement using link_map data.
    In order to update the symbolic links in the filesystem they
    must be removed and regenerated.
    return: None
    """
    for link in link_map:
        if not os.path.exists(link.path):
            print("WARNING :: Destination {0} does not exist.  May generate a dead link.".format(link.destination))
        
        print("Re-linking: {0} -> {1}".format(link.path, link.destination))
        os.unlink(link.path)
        os.symlink(link.destination, link.path)

def user_choice():
    """Would you like to play a game?
    """
    YES = ['YES', 'Y']
    choice = raw_input().upper()
    if choice not in YES:
        return False
    return True

def trap_sigint(sig, frame):
    """Perform any cleanup tasks (most likely none)
    """
    print("Caught signal {0}.  Exiting.".format(sig))
    exit(0)
    
if __name__ == "__main__":
    signal.signal(signal.SIGINT, trap_sigint)
    parser = argparse.ArgumentParser(description='Replace symbolic links')
    parser.add_argument('--force', action="store_true", help='Do not ask for confirmation')
    parser.add_argument('search', type=str, action="store", help='original path pattern')
    parser.add_argument('replace', type=str, action="store", help='replacement string in path pattern')
    parser.add_argument('path', type=str, action="store", help='path to perform search')
    args = parser.parse_args()
    
    if args.search == args.replace or \
        args.replace == args.search:
        print("Search and replace patterns cannot be the same.")
        exit(1)
    
    lmap = generate_link_map(os.walk(os.path.abspath(args.path), followlinks=False))
    if not lmap:
        print("No symbolic links detected under {0}".format(args.path))
        exit(0)
    
    print("### LINKS ####")
    for link in get_link_map(lmap):
        print("Link: {0} -> {1}".format(link.path, link.destination))

    rmap = generate_replacement_link_map(lmap, args.search, args.replace)
    if not rmap:
        print("Nothing to replace.".format(args.search, args.path))
        exit(0)
    
    print("")
    print("### PREVIEW ####")
    for link in get_link_map(rmap):
        print("Replacement: {0} -> {1}".format(link.path, link.destination))
    
    link_total = len(lmap)
    match_total = len(rmap)
    print("")
    print("{0} symbolic links total, {1} pattern matches".format(link_total, match_total))
    print("")
    if args.force:
        replace_links(rmap)
    else:
        print("Replace symbolic link(s)? [y/N]"),
        if user_choice():
            print("")
            replace_links(rmap)
        else:
            print("Abort.")
            exit(254)            
    exit(0)