4

I have a folder system with the structure below:

folderA 
- folder1
  - file1A.txt
- folder2
  - file2A.txt
- folder3
  - file3A.txt

folderB 
- folder1
  - file1B.txt 
- folder2 
  - file2B.txt 
- folder3
  - file3B.txt

I wish to change the order to make the numbered folder above the letter folders as:

folder1 
- folderA
  - file1A.txt
- folderB
  - file1B.txt

folder2 
- folderA
  - file2A.txt 
- folderB 
  - file2B.txt 

folder3 
- folderA
  - file3A.txt 
- folderB 
  - file3B.txt 

Here is a piece of code to construct a MWE of the initial directory structure:

import os 
import shutil
import string

root_dir = os.getcwd()
os.chdir('/home/alletro/Tc-97/tools')
os.makedirs('master', exist_ok=True)
os.chdir('master') 
master_dir = os.getcwd()
top_tier = [f'folder{i}' for i in range(1,4)]
second_tier = [f'folder{i}' for i in list(string.ascii_uppercase)[:4]]
for folder in top_tier: 
    os.chdir(master_dir)
    os.makedirs(folder, exist_ok=True) 
    os.chdir(folder)
    fold_dir = os.getcwd()
    for sub_folder in second_tier:
        os.chdir(fold_dir)
        os.makedirs(sub_folder, exist_ok=True) 
        os.chdir(sub_folder)
        os.mknod("newfile.txt")
os.chdir(root_dir)

I have found a solution that gets me a dictionary of the directory tree:

def get_directory_structure(rootdir):
    """
    Creates a nested dictionary that represents the folder structure of rootdir
    """
    dir = {}
    rootdir = rootdir.rstrip(os.sep)
    start = rootdir.rfind(os.sep) + 1
    for path, dirs, files in os.walk(rootdir):
        folders = path[start:].split(os.sep)
        subdir = dict.fromkeys(files)
        parent = reduce(dict.get, folders[:-1], dir)
        parent[folders[-1]] = subdir
    return dir

I'm however struggling to see where to take it from here.

Allentro
  • 406
  • 2
  • 13

3 Answers3

2

The below worked for me. There's four parts:

  1. Get list of all file paths.
  2. Use that list to create list of desired subfolders.
  3. Create the subfolders and move files into them.
  4. Delete the old file hierarchy.

Part 1: Get list of file paths.

import os
import glob
import shutil

all_files = [filename for filename in glob.iglob('**/*.txt', recursive=True)]

Part 2: Use the filenames to create list of subfolders.

# Function that takes a filename and returns desired subfolder and file paths as tuple. 
def get_full_path(file):
    num = file[-6]
    let = file[-5]
    file_name = file.split('/')[-1]
    
    subfolders = f'folder{num}/folder{let}/'
    file_path = f'folder{num}/folder{let}/{file_name}'
    return subfolders, file_path

# Call the function on each file to get all subfolders and file paths.
subfolders = []
full_paths = []
for file in all_files:
    subfolder, full_path = get_full_path(file)
    subfolders.append(subfolder), full_paths.append(full_path)

Part 3: Create all subfolders, move all files into them.

for i in range(len(all_files)):
    os.makedirs(subfolders[i])
    os.rename(all_files[i], full_paths[i])

Part 4: Delete the original folders and subfolders.

old_folders = [x for x in os.listdir() if x[-1] in ['A', 'B', 'C']]
for folder in old_folders:
    shutil.rmtree(folder, ignore_errors=False, onerror=None)

If you have folders beyond folder 'C', you'll need to expand the in ['A', 'B', 'C'] list in part 4. Otherwise this should scale arbitrarily.

Let me know if you have any questions, and good luck with the rest of the project!

BLimitless
  • 2,060
  • 5
  • 17
  • 32
1

Here's a solution that doesn't assume specific folder names:

from pathlib import Path

def switch_parts(path):
    one, two, three, four = path.parts
    return Path(one, three, two, four)

# generate new paths
files = Path('master').glob('**/*.txt')
rearranged = [switch_parts(f) for f in files]

# create new folders and move files
for old, new in zip(files, rearranged):
    new.parent.mkdir(parents=True, exist_ok=True)
    old.rename(new)

# clean up old folders
for old in files:
    old.parent.rmdir()
    try:
        old.parent.parent.rmdir()
    except OSError:
        pass # will be deleted eventually
Jan Wilamowski
  • 3,308
  • 2
  • 10
  • 23
  • There would be some issues when the old and the rearranged paths are not distinct sets (try with `.../folderX/folderX/...`), but I like the idea (+1) – VPfB May 18 '21 at 09:31
  • @VPfB good point. That risk seems to be the price for the added flexibility. – Jan Wilamowski May 18 '21 at 09:56
0

You can first get all the paths to the files, and then use recursion to create the new structure:

import os, shutil, collections
#generator function that finds all file paths in the target directory
def get_files(d = os.getcwd(), p = None):
  for i in os.listdir(d):
     if os.path.isdir(_d:=os.path.join(d, i)):
        yield from get_files(d = _d, p = ([] if p is None else p)+[i])
     else:
        yield ([] if p is None else p)+[i]

#group all reversed file paths from the function above on the leading dir name
def new_structure(d, p = []):
   _dir = collections.defaultdict(list)
   for a, b, c in d:
      if not a:
         shutil.copyfile(c, os.path.join(os.getcwd(), *p, b))
      else:
         _dir[a[0]].append((a[1:], b, c))
   for a, b in _dir.items():
      os.mkdir(os.path.join(os.getcwd(), *(_p:=p+[a])))
      new_structure(b, p = _p)

r = [[a[::-1], b, os.path.join(*a, b)] for *a, b in get_files()]
new_structure(r)
#remove old structure
for (*_, a), _, _ in r:
   if os.path.isdir(a):
      shutil.rmtree(a, ignore_errors=False, onerror=None)

This solution will work with directories of any depth and with entirely arbitrary directory and file names.

Ajax1234
  • 69,937
  • 8
  • 61
  • 102