Moving files into sub folders based by average filesize - powershell

I need the help of you programming savants in creating a batch script or powershell script that will move and divide a group of files from one directory into 4 subdirectories based on an average total filesize. After the sort, the sub-directories should be roughly equal in terms of folder size.
Why do I need this?
I have 4 computers that I would like to utilize for encoding via FFMPEG and it would be helpful for a script to divide a folder into 4 parts (sub-directories) based on a total average size.
So lets say there are an assortment of movie files with varying different file sizes totaling to 100 GB, the script would divy the movie files and move them into 4 sub folders; each folder having around 25 GB. Doing this will allow the 4 machines to encode the sum of the data equally and efficiently.
After all that encoding I'll have 2 files, XYZ.(original Extension) and XYZ.264, A script that could compare the 2 files and delete the larger file would be extremely helpful and cut down on manual inspection.
Thank you, I hope this is possible.

#ECHO Off
SETLOCAL ENABLEDELAYEDEXPANSION
SET "sourcedir=U:\sourcedir"
SET "destdir=U:\destdir"
PUSHD "%sourcedir%"
:: number of subdirectories
SET /a parts=4
:: make subdirs and initialise totalsizes
FOR /L %%a IN (1,1,%parts%) DO MD "%destdir%\sub%%a" 2>nul&SET /a $%%a=0
:: directory of sourcefiles, sort in reverse-size order
FOR /f "tokens=1*delims=" %%a IN (
'dir /b /a-d /o-s * '
) DO (
REM find smallest subdir by size-transferred-in
SET /a smallest=2000000000
FOR /L %%p IN (1,1,%parts%) DO IF !$%%p! lss !smallest! SET /a smallest=!$%%p!&SET part=%%p
REM transfer the file and count the size
ECHO(MOVE "%%a" "%destdir%\sub!part!"
REM divide by 100 as actual filelength possibly gt 2**31
SET "size=%%~za"
IF "!size:~0,-2!" equ "" (SET /a $!part!+=1) ELSE (SET /a $!part!=!size:~0,-2! + $!part!)
)
popd
GOTO :EOF
I believe the remarks should explain the method. The principle is to record the length-transferred to each subdirectory and select the least-filled as the destination for the file (processed in reverse-size order)
Since batch has a limit of 2^31, I chose to roughly divide the filesize by 100 by lopping of the last 2 digits. For files <100 bytes, I arbitrarily recorded that as 100 bytes.
You would need to change the settings of sourcedir and destdir to suit your circumstances.
The required MOVE commands are merely ECHOed for testing purposes. After you've verified that the commands are correct, change ECHO(MOVE to MOVE to actually move the files. Append >nul to suppress report messages (eg. 1 file moved)
#ECHO OFF
SETLOCAL ENABLEDELAYEDEXPANSION
SET "destdir=U:\destdir"
SET "spaces= "
FOR /f "delims=" %%a IN (
'dir /b /ad "%destdir%\*"'
) DO (
PUSHD "%destdir%\%%a"
FOR /f "delims=" %%f IN (
'dir /b /a-d "*.xyz" 2^>nul'
) DO (
IF EXIST "%%f.264" (
FOR %%k IN ("%%f.264") DO (
SET "sizexyz=%spaces%%%~zf"
SET "size264=%spaces%%%~zk"
IF "!sizexyz:~-15!" gtr "!size264:~-15!" (ECHO(DEL /F /Q "%%f") ELSE (ECHO(DEL /F /Q "%%f.264")
)
)
)
popd
)
GOTO :EOF
This second batch scans the directorynames into %%a then switches teporarily to the detination directory %destfile\%%a.
Once there, we look for .xyz files and for each one found, find the corresponding .xyz.264 file.
If that exists, then we find the sizes of the files (%%~zk or %%~zf) and append that to a long string of spaces. By comparing the last 15 characters of the result as a string, we can determine which is longer.
The required DEL commands are merely ECHOed for testing purposes. After you've verified that the commands are correct, change ECHO(DEL to DEL to actually delete the files.
If the .264 file is filename.264 instead of filename.xyz.264 then replace each "%%f.264" with "%%~nf.264" (the ~n selects the name-part only).
To manually enter a source directoryname, use
SET /p "sourcedir=Source directory "
To accept the source directoryname as a parameter, use
SET "sourcedir=%%~1"
To process all files, except .h264 files, change
FOR /f "delims=" %%f IN (
'dir /b /a-d "*.xyz" 2^>nul'
) DO (
to
FOR /f "delims=" %%f IN (
'dir /b /a-d "*.*" 2^>nul'
) DO if /i "%%~xf" neq ".h264" (
where *.* means "all files" and the extra if statement checks whether the extension to the filename %%f (%%~xf) is not equal to (neq) .h264 and the /i directs "regardless of case (case-Insensitive)"

This might seem like a simple request, but exact partitioning is actually a really hard problem.
The easiest way to approximate a somewhat fair partitioning is simply to sort all files (from biggest to smallest) and then distribute them one-by-one into n groups (a bit like if you were giving out cards for a card game):
# Define number of subgroups/partitions
$n = 4
# Create your destination folders:
$TargetFolders = 1..$n |ForEach-Object {
mkdir "C:\path\to\movies\sub$_"
}
# Find the movie files sort by length, descending
$Files = Get-ChildItem "C:\path\to\movies" -Recurse |Where-Object {'.mp4','.mpg','.xyz' -contains $_.Extension} |Sort-Object Length -Descending
for($i = 0; $i -lt $Files.Count; $i++)
{
# Move files into sub folders, using module $n to "rotate" target folder
Move-Item $Files[$i].FullName -Destination $TargetFolders[$i % $n]
}
If you have multiple file types that you want to include, use a Where-Object filter instead of the Filter parameter with Get-ChildItem:
$Files = Get-ChildItem "C:\path\to\movies" -File -Recurse |Where-Object {'.mp4','.mpg','.xyz' -contains $_.Extension} |Sort-Object Length -Descending

#!/bin/bash
nbr_of_dirs=4
# Go to directory if specified, otherwise execute in current directory
if [ -n "$1" ]; then
cd $1
fi
# Create output directories and store them in an array
for i in $(seq 1 $nbr_of_dirs); do
dir=dir_$i
mkdir $dir
dirs[i]=$dir
done
# For every non-directory, in decreasing size:
# find out the current smallest directory and move the file there
ls -pS | grep -v / | while read line; do
smallest_dir=$(du -S ${dirs[#]} | sort -n | head -1 | cut -f2)
mv "$line" $smallest_dir
done
Remember to keep the script file in a different directory when executing this. The script iterates over every file, so if the script was in the directory too it would be moved to one of the sub-directories.

Related

Renaming incrementing integer name to incrementing integer

can you show me a way to solve following problem with Powershell or CMD?
This is my file names in folder.
002.mp3
003.mp3
.
.
.
604.mp3
I want to change that numbers to;
001.mp3
002.mp3
003.mp3
.
.
.
603.mp3
But important thing is, it has to be like that
002.mp3 to 001.mp3
003.mp3 to 002.mp3
...
...
...
604.mp3 to 603.mp3
thank you very much.
You can do the following:
$files = Get-ChildItem -Path "C:\PathToMP3Files\[0-6][0-9][0-9].mp3" -File
$files | Rename-Item -NewName {"{0:D3}.mp3" -f ([int]$_.BaseName - 1)} -whatif
Just remove the -whatif parameter to perform the rename if you are satisfied with the results.
Explanation:
The -Path parameter of Get-ChildItem supports wildcards. Using a wildcard range [0-6] (one character from the set 0 to 6), you can narrow down your target items.
Since New-Item's -NewName supports delay-script binding, you can pipe your FileInfo objects directly into the command. The -NewName parameter manipulates the Name property of the object. The code above is using BaseName, which is the Name without the extension, because it is an easy way to perform digit increase.
-f is the string format operator. It performs a substitution of {number} values within a string. The number corresponds to an item's index in the collection provided to the right of the -f.
This is simpler in a Batch file, and run faster than PS too! (just try it)
#echo off
setlocal EnableDelayedExpansion
for /F %%a in ('dir /B *.mp3') do set /A "n=1%%~Na-1" & ren "%%a" "!n:~1!.mp3"
Yoy may even do it directly at the command prompt with no need of a .bat file; just be sure that n variable does not exists (executing set "n=" before):
for /F %a in ('dir /B *.mp3') do #set /A "n=1%~Na-1" & call ren "%a" "%n:~1%.mp3"

batch add a character(s) in the middle of a filename using cmd

So I have files (mostly documents) with a file name beginning with "YYYYMMDD - Title of file.etc"
I'm wanting to change the date format to YYYY-MM-DD
I'm wanting to do a script to batch rename since I'll be doing this every now and then.
My bat code so far after a 2 days of research to no avail:
for %%a in ("*.*") do ren *.* ????-??-??*.*
pause
Any help or point to the right direction for me to look at would be really helpful. Thanks
final
rem // Loop through all matching files:
for /F "eol=| delims=" %%F in ('
dir /B /A:-D-H-S "*.*"
') do (
rem // Store the current file name and extension to variables:
set "NAME=%%~F"
rem // Enable delayed expansion to avoid trouble with `!`:
setlocal EnableDelayedExpansion
rem // Rename file by applying sub-string expansion:
ren "!NAME!" "!NAME:~,4!-!NAME:~4,2!-!NAME:~6!!EXT!"
endlocal
)
pause
You are utilising a for loop but not using its meta-variable %%a in the body then, which makes no sense.
Unfortunately, you cannot just use ren for that, because every ? in the new name represents the character of the old name at that position.
I would do it the following way:
rem // First change to the target directory:
cd /D "D:\Target\Dir"
rem // Loop through all matching files:
for /F "eol=| delims=" %%F in ('
dir /B /A:-D-H-S "*.*" ^| findstr /I "^[1-2][0-9][0-9][0-9][0-1][0-9][0-3][0-9]"
') do (
rem // Store the current file name and extension to variables:
set "NAME=%%~nF" & set "EXT=%%~xF"
rem // Enable delayed expansion to avoid trouble with `!`:
setlocal EnableDelayedExpansion
rem // Rename file by applying sub-string expansion:
ren "!NAME!!EXT!" "!NAME:~,4!-!NAME:~4,2!-!NAME:~6!!EXT!"
endlocal
)
The [findstr][cmdfnd] is used here to filter for file names that begin with eight decimal digits that could represent a date. If you do not want that simply remove everything from ^| findstr up to the end of that line.
Delayed variable expansion is needed here, because you are writing and reading variables within the same block of code. Splitting the original name at fixed character positions is done by sub-string expansion.
(How-To: Extract part of a variable (substring))
[cmdfnd]: https://ss64.com/nt/findstr.html
(FINDSTR)
RightHanded,
Maybe you should look at Powershell commands.
The following powershell command replaces spaces with underscores of all files of the current directory :
dir | rename-item -NewName {$_.name -replace " ","_"}
Source : https://www.howtogeek.com/111859/how-to-batch-rename-files-in-windows-4-ways-to-rename-multiple-files/
You can see how to modify Powershell strings here for example : https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.utility/convert-string?view=powershell-5.1
Powershell will be much easier to do this. Use this:
#echo off
pushd "The Directory"
powershell -command "& {get-childitem | foreach {$NewName = $_.Name -replace '\d{8}.*', '\d{4}-\d{2}-\d{2}.*'; rename-item -path '.\$_.Name' -newname '$NewName'}}"
popd
goto :eof
You can use this as a function to rename the files to the format you want. You can call this function inside a loop FOR, if you want.
set old_filename=YYYYMMDD - Title of file.etc
:: Extract only the first 4 characters
SET yyyy=%old_filename:~0,4%
:: Skip 4 characters and then extract the next 2
SET mm=%old_filename:~4,2%
:: Skip 6 characters and then extract the next 2
SET dd=%old_filename:~6,2%
:: Skip 8 characters and then extract everything else
SET everything_else=%old_filename:~8%
:: Rename de old filename to the new
rename "%old_filename%" "%yyyy%-%mm%-%dd%%everything_else%"

Changing Multiple Filenames in multiple Folders with Batch

I have several Folder which contain from 5 to 20 Files with all different names. They get replaced every week, so the Name of each File also changes. But i Need them to have specific Names so i can upload them by using my SQL loader.
Is there a way to create a Batch file, which goes into every Folder that i specify, select all Files and changes all the names? Perfect Solution would just be a upcounting number like: file1.xml, file2.xml etc.
Since im a total newbie to Batch i searched around a bit and found following code, but it only changes the files in 1 specific Folder.
Dir *.xml | ForEach-Object -begin { $count=1 }
-process { rename-item $_ -NewName "$count.xml"; $count++ }
Update 1
I found the working code which allows me to rename the files in a Folder as i want them to be. I would just Need a code, that allows me to do this to several other Folders at the same time or automatically one after another.
#echo off & setlocal EnableDelayedExpansion
set a=1
for /f "delims=" %%i in ('dir /b *.xml') do (
ren "%%i" "!a!.xml"
set /a a+=1
)
#echo off & setlocal EnableDelayedExpansion
set a=1
rem make old name to prevent same name collision
for /D %%i in (*) do (
cd %%i
for /f "delims=" %%j in ('dir /b *.xml') do (
ren "%%j" "%%j-old.xml"
)
cd ..
)
rem rename process
for /D %%i in (*) do (
cd %%i
for /f "delims=" %%j in ('dir /b *.xml') do (
ren "%%j" "!a!.xml"
set /a a+=1
)
cd ..
)
echo Done
pause
Start .
hope it helps.
Here's a PowerShell code to do the same for several folders (sub-folders)
$path = 'Z:\'
Get-ChildItem -Path $path -Filter *.xml -Recurse | ForEach-Object -Begin {
[int]$count = '1'
} -Process {
Rename-Item -Path $_.FullName -NewName "$count.xml" -ErrorAction Stop
$count++
}

How to interrogate multiple text files for certain key words and output results in text?

Here is my objective:
I have a networked folder (Windows 7) that contains text log files that are generated on daily basis (1 log text per day), as a result file names contain various dates.
I need a script to go in and scan each new log file for a list of certain keywords: "ABC", "DEF", "GHI". Then write out how many of these events were found in each log file and save this info in a text format or similar.
Issues I'm facing: how do i get around various file names, each file name contains that days date. How do i look through a list of keywords and count all of these words in each log file. How do i write out the results in a separate text file. And finally is there a way to automate all of this?
I would like to stay in PowerShell or use Windows batch files.
I am afraid your request is not clear and have several missing details. However, here it is a Batch file with what I guess could be the first attempt to get a solution:
EDIT: I modified the original code in order to also group the files per event; the first code just groups events per file.
#echo off
setlocal EnableDelayedExpansion
rem Push dir to the networked folder
pushd "\\Path\to\networked\folder"
rem Count all file/event combinations in a two-dimensional array
for %%a in (ABC DEF GHI) do (
for /F "delims=:" %%b in ('findstr /I /L "%%a" logFiles*.txt') do (
set /A "count[%%~Nb][%%a]+=1"
)
)
rem Group events for the same file
for /F "tokens=2,3* delims=[]=" %%a in ('set count[') do (
set "file[%%a]=!file[%%a]!, %%b (%%c)"
)
rem Show the final result 1: events per file
(
echo Events per file:
for /F "tokens=2* delims=[]=" %%a in ('set file[') do (
set "line=%%b"
echo %%a.txt = !line:~2!
)
) > result1.txt
rem Group and count files for the same event
for /F "tokens=2,3* delims=[]=" %%a in ('set count[') do (
set "event[%%b]=!event[%%b]!, %%a.txt (%%c)"
set /A "eventCount[%%b]+=%%c"
)
rem Show the final result 2: files per event
(
echo Files per event:
for /F "tokens=2* delims=[]=" %%a in ('set event[') do (
set "line=%%b"
echo "%%a" - !eventCount[%%a]! found in !line:~2!
)
) > result2.txt
In PowerShell we can use Get-Childitem to collect which files we want to search though and store those in a variable as such:
$Content = Get-Content (Get-Childitem -Path C:\Temp\*.txt)
Once those text files are stored into our Content variable we can simply use the -match regex operator to find a match inside that collection of files.
$Content -match "Bacon"
You could then out this information using Out-File:
$Content -match "Bacon" | Out-file C:\Temp\Bacon.txt
Of course with the Get-child item you can use the DateTime operator and narrow down your query for only recently changed text files as required.
If you want to get more technical we can use Select-String to get this output for multiple matches as such:
Get-ChildItem C:\Logs |
where { $_ | Select-String -Pattern 'Blahblah' } |
where { $_ | Select-String -Pattern '\d{4}' } |
...
You can also do it on one line:
gci C:\Logs| select-string -pattern '(Bacon.*Failed)|(Failed.*Steak)'
Outputting is up to your imagination really. Once they are just simple strings they can be put into an array or a custom PS object for nice output. it would just require you to save the command you were running into a variable then call it later for the export.
for Grouping, take a look at Group-Object cmdlet. it will give you the occurrence numbers.

Flatten Files in Subfolders on Windows 7 (cmd or PowerShell)

I have a large volume of files organized in a very hierarchical folder structure. In this structure, the file that I care about is always located in the lowest level of the folders. As such, I'd like to flatten the directory so that it's easier to access the files that I care about. However, I'd like to preserve the 2 higher levels (Person & Project) of the folder structure.
Here's an example of the EXISTING folder directory:
Directory
Tom
Project 1
Subfolder Level A
FileA
FileB
Project 2
Subfolder Level C
FileC
FileD
Jerry
Project 1
Subfolder Level E
FileE
Here's an example of the DESIRED folder directory:
Directory
Tom
Project 1
FileA
FileB
Project 2
FileC
FileD
Jerry
Project 1
FileE
I have tried doing something like this, however this flattens all of the files into a single directory:
for /r %f in (*) do #copy "%f" .
However, this produces:
Directory
FileA
FileB
FileC
FileD
FileE
I'd appreciate any guidance that you can offer. Thanks a lot!
Here is a Powershell approach. It gets a list of the folders at the level that you want. Then it moves all the sub files up to that level. it will also remove the sub folders.
$Rootfolder = Dir directory\*\* -Directory
ForEach($folder in $Rootfolder)
{
Dir $folder.fullname -Recurse -File | Copy-Item -Destination $folder.fullname
Dir $folder.fullname -Recurse -Directory | Remove-Item -Force -Recurse -WhatIf
}
If you want it to delete, remove the -WhatIf from the last line.
Throw a couple of extra for loops around the one that works then.
e.g. First change to the name folder, then the project folder, looping through both levels.
for /D %n in (*) do (
pushd %n
for /D %p in (*) do (
pushd %p
for /r %f in (*) do #copy "%f" .
popd
)
popd
)
If you put this in a bat file, remember to replace % with %%
#echo off
pushd yourDirectory
for /d %%A in (*) do for /d %%B in ("%%A\*") do for /d %%C in ("%%B\*") do (
pushd "%%C"
for /r %%F in (*) do move /y "%%F" "%%B" >nul
popd
rd /q /s "%%C"
)
popd
%%A contains something like "yourDirectory\Tom"
%%B contains something like "yourDirectory\Tom\Project 1"
%%C contains something like "yourDirectory\Tom\Project 1\subdirectory1"
%%F contains a file to move, to any depth
At first I thought I could eliminate PUSHD/POPD and use
for /r "%%C" %%F in (*) do ... THIS DOES NOT WORK
But that doesn't work - the value after /R cannot use a FOR variable or delayed expansion because of how the command is parsed.
I tweaked #ScottC's answer and used the following code:
for /D %%n in (*) do (
pushd %%n
for /D %%p in (*) do (
pushd %%p
for /r %%f in (*.ppt) do (
move "%%f" "[ROOT_PATH_THAT_I_WANT]\%%n\%%p".
)
popd
)
popd
)
I ran this solution as a .bat file, which is why I used %% instead of %.
%%n = name (aka C:\Directory\Name)
%%p = project (aka C:\Directory\Name\Project)
%%f = file to be moved (recursively drilling through the folders and moving them up to the project level)
Ultimately, I wasn't able to get #dbenham's suggestion of deleting the empty folders to work, so I ended up using this utility: http://www.jonasjohn.de/red.htm. So far it seems pretty intuitive and like it's taking care of the problem without much effort from me :)
Thanks for the help everybody!