Skip to content

Commit 7c6f87c

Browse files
committed
RenameFiles : Add new TaskNode for renaming files
1 parent 3799287 commit 7c6f87c

10 files changed

Lines changed: 1048 additions & 0 deletions

File tree

Changes.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ Features
99
- FileList : Added node for listing matching files.
1010
- DeleteFiles : Added node for deleting files.
1111
- CopyFiles : Added node for copying files.
12+
- RenameFiles : Added node for renaming files.
1213

1314
Improvements
1415
------------
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
//////////////////////////////////////////////////////////////////////////
2+
//
3+
// Copyright (c) 2025, Cinesite VFX Ltd. All rights reserved.
4+
//
5+
// Redistribution and use in source and binary forms, with or without
6+
// modification, are permitted provided that the following conditions are
7+
// met:
8+
//
9+
// * Redistributions of source code must retain the above
10+
// copyright notice, this list of conditions and the following
11+
// disclaimer.
12+
//
13+
// * Redistributions in binary form must reproduce the above
14+
// copyright notice, this list of conditions and the following
15+
// disclaimer in the documentation and/or other materials provided with
16+
// the distribution.
17+
//
18+
// * Neither the name of John Haddon nor the names of
19+
// any other contributors to this software may be used to endorse or
20+
// promote products derived from this software without specific prior
21+
// written permission.
22+
//
23+
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
24+
// IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
25+
// THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
26+
// PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
27+
// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
28+
// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
29+
// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
30+
// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
31+
// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
32+
// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
33+
// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
34+
//
35+
//////////////////////////////////////////////////////////////////////////
36+
37+
#pragma once
38+
39+
#include "GafferDispatch/TaskNode.h"
40+
41+
#include "Gaffer/StringPlug.h"
42+
#include "Gaffer/TypedObjectPlug.h"
43+
44+
namespace GafferDispatch
45+
{
46+
47+
class GAFFERDISPATCH_API RenameFiles : public TaskNode
48+
{
49+
50+
public :
51+
52+
explicit RenameFiles( const std::string &name=defaultName<RenameFiles>() );
53+
~RenameFiles() override;
54+
55+
GAFFER_NODE_DECLARE_TYPE( GafferDispatch::RenameFiles, RenameFilesTypeId, TaskNode );
56+
57+
Gaffer::StringVectorDataPlug *filesPlug();
58+
const Gaffer::StringVectorDataPlug *filesPlug() const;
59+
60+
Gaffer::StringPlug *namePlug();
61+
const Gaffer::StringPlug *namePlug() const;
62+
63+
Gaffer::StringPlug *deletePrefixPlug();
64+
const Gaffer::StringPlug *deletePrefixPlug() const;
65+
66+
Gaffer::StringPlug *deleteSuffixPlug();
67+
const Gaffer::StringPlug *deleteSuffixPlug() const;
68+
69+
Gaffer::StringPlug *findPlug();
70+
const Gaffer::StringPlug *findPlug() const;
71+
72+
Gaffer::StringPlug *replacePlug();
73+
const Gaffer::StringPlug *replacePlug() const;
74+
75+
Gaffer::BoolPlug *useRegularExpressionsPlug();
76+
const Gaffer::BoolPlug *useRegularExpressionsPlug() const;
77+
78+
Gaffer::StringPlug *addPrefixPlug();
79+
const Gaffer::StringPlug *addPrefixPlug() const;
80+
81+
Gaffer::StringPlug *addSuffixPlug();
82+
const Gaffer::StringPlug *addSuffixPlug() const;
83+
84+
Gaffer::BoolPlug *replaceExtensionPlug();
85+
const Gaffer::BoolPlug *replaceExtensionPlug() const;
86+
87+
Gaffer::StringPlug *extensionPlug();
88+
const Gaffer::StringPlug *extensionPlug() const;
89+
90+
Gaffer::BoolPlug *overwritePlug();
91+
const Gaffer::BoolPlug *overwritePlug() const;
92+
93+
protected :
94+
95+
IECore::MurmurHash hash( const Gaffer::Context *context ) const override;
96+
void execute() const override;
97+
98+
private :
99+
100+
static size_t g_firstPlugIndex;
101+
102+
// Friendship for the bindings
103+
friend struct GafferDispatchBindings::Detail::TaskNodeAccessor;
104+
105+
};
106+
107+
} // namespace GafferDispatch

include/GafferDispatch/TypeIds.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ enum TypeId
5050
FileListTypeId = 118805,
5151
DeleteFilesTypeId = 118806,
5252
CopyFilesTypeId = 118807,
53+
RenameFilesTypeId = 118808,
5354

5455
LastTypeId = 118999
5556

Lines changed: 263 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,263 @@
1+
##########################################################################
2+
#
3+
# Copyright (c) 2025, Cinesite VFX Ltd. All rights reserved.
4+
#
5+
# Redistribution and use in source and binary forms, with or without
6+
# modification, are permitted provided that the following conditions are
7+
# met:
8+
#
9+
# * Redistributions of source code must retain the above
10+
# copyright notice, this list of conditions and the following
11+
# disclaimer.
12+
#
13+
# * Redistributions in binary form must reproduce the above
14+
# copyright notice, this list of conditions and the following
15+
# disclaimer in the documentation and/or other materials provided with
16+
# the distribution.
17+
#
18+
# * Neither the name of John Haddon nor the names of
19+
# any other contributors to this software may be used to endorse or
20+
# promote products derived from this software without specific prior
21+
# written permission.
22+
#
23+
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
24+
# IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
25+
# THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
26+
# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
27+
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
28+
# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
29+
# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
30+
# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
31+
# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
32+
# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
33+
# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
34+
#
35+
##########################################################################
36+
37+
import unittest
38+
39+
import IECore
40+
41+
import Gaffer
42+
import GafferDispatch
43+
import GafferTest
44+
45+
class RenameFilesTest( GafferTest.TestCase ) :
46+
47+
def testSourceVariable( self ) :
48+
49+
path1 = self.temporaryDirectory() / "test1"
50+
path2 = self.temporaryDirectory() / "test2"
51+
path1.touch()
52+
path2.touch()
53+
54+
spreadsheet = Gaffer.Spreadsheet()
55+
spreadsheet["selector"].setValue( "${source}" )
56+
spreadsheet["rows"].addColumn( Gaffer.StringPlug( "name" ) )
57+
spreadsheet["rows"].addRows( 2 )
58+
spreadsheet["rows"][0]["name"].setValue( str( path1 ) )
59+
spreadsheet["rows"][0]["cells"]["name"]["value"].setValue( "apple" )
60+
spreadsheet["rows"][1]["name"].setValue( str( path2 ) )
61+
spreadsheet["rows"][1]["cells"]["name"]["value"].setValue( "pear" )
62+
63+
rename = GafferDispatch.RenameFiles()
64+
rename["files"].setValue( IECore.StringVectorData( [ str( path1 ), str( path2 ) ] ) )
65+
rename["name"].setInput( spreadsheet["out"]["name"] )
66+
rename["task"].execute()
67+
68+
self.assertEqual(
69+
{ p.name for p in self.temporaryDirectory().glob( "*" ) },
70+
{ "apple", "pear" }
71+
)
72+
73+
def testStemAndExtensionVariables( self ) :
74+
75+
path = self.temporaryDirectory() / "test.tar"
76+
path.touch()
77+
78+
rename = GafferDispatch.RenameFiles()
79+
rename["files"].setValue( IECore.StringVectorData( [ str( path ) ] ) )
80+
rename["name"].setValue( "${source:stem}Suffix${source:extension}.gz" )
81+
rename["task"].execute()
82+
83+
self.assertEqual(
84+
[ p.name for p in self.temporaryDirectory().glob( "*" ) ],
85+
[ "testSuffix.tar.gz" ]
86+
)
87+
88+
def testChangeAffixes( self ) :
89+
90+
path = self.temporaryDirectory() / "prefixMiddleSuffix.ext"
91+
path.touch()
92+
93+
rename = GafferDispatch.RenameFiles()
94+
rename["files"].setValue( IECore.StringVectorData( [ str( path ) ] ) )
95+
rename["deletePrefix"].setValue( "prefix" )
96+
rename["deleteSuffix"].setValue( "Suffix" )
97+
rename["addPrefix"].setValue( "newBeginning" )
98+
rename["addSuffix"].setValue( "NewEnding" )
99+
rename["task"].execute()
100+
101+
self.assertEqual(
102+
[ p.name for p in self.temporaryDirectory().glob( "*" ) ],
103+
[ "newBeginningMiddleNewEnding.ext" ]
104+
)
105+
106+
def testFindReplace( self ) :
107+
108+
path = self.temporaryDirectory() / "aBaBaBA.abc"
109+
path.touch()
110+
111+
rename = GafferDispatch.RenameFiles()
112+
rename["files"].setValue( IECore.StringVectorData( [ str( path ) ] ) )
113+
rename["find"].setValue( "a" )
114+
rename["replace"].setValue( "C" )
115+
rename["task"].execute()
116+
117+
self.assertEqual(
118+
[ p.name for p in self.temporaryDirectory().glob( "*" ) ],
119+
[ "CBCBCBA.abc" ]
120+
)
121+
122+
def testRegularExpressions( self ) :
123+
124+
path = self.temporaryDirectory() / "abc123.ext"
125+
path.touch()
126+
127+
rename = GafferDispatch.RenameFiles()
128+
rename["files"].setValue( IECore.StringVectorData( [ str( path ) ] ) )
129+
rename["find"].setValue( "a*c" )
130+
rename["replace"].setValue( "{0}d" )
131+
rename["useRegularExpressions"].setValue( True )
132+
rename["task"].execute()
133+
134+
self.assertEqual(
135+
[ p.name for p in self.temporaryDirectory().glob( "*" ) ],
136+
[ "abcd123.ext" ]
137+
)
138+
139+
def testNameOverridesEverything( self ) :
140+
141+
path = self.temporaryDirectory() / "prefixMiddleSuffix.ext"
142+
path.touch()
143+
144+
rename = GafferDispatch.RenameFiles()
145+
rename["files"].setValue( IECore.StringVectorData( [ str( path ) ] ) )
146+
rename["name"].setValue( "newName.abc" )
147+
rename["deletePrefix"].setValue( "prefix" )
148+
rename["deleteSuffix"].setValue( "Suffix" )
149+
rename["find"].setValue( "Middle" )
150+
rename["replace"].setValue( "NewMiddle" )
151+
rename["addPrefix"].setValue( "newPrefix" )
152+
rename["addSuffix"].setValue( "NewSuffix" )
153+
rename["task"].execute()
154+
155+
self.assertEqual(
156+
[ p.name for p in self.temporaryDirectory().glob( "*" ) ],
157+
[ "newName.abc" ]
158+
)
159+
160+
def testConflictingNames( self ) :
161+
162+
source1 = self.temporaryDirectory() / "path1"
163+
source1.write_text( "1" )
164+
165+
source2 = self.temporaryDirectory() / "path2"
166+
source2.write_text( "2" )
167+
168+
destination = self.temporaryDirectory() / "path"
169+
170+
rename = GafferDispatch.RenameFiles()
171+
rename["files"].setValue( IECore.StringVectorData( [ str( source1 ), str( source2 ) ] ) )
172+
rename["name"].setValue( "path" )
173+
174+
with self.assertRaisesRegex( RuntimeError, f'.*Destination ".*/{destination.name}" has multiple source files : ".*/{source1.name}" and ".*/{source2.name}"' ) :
175+
rename["task"].execute()
176+
177+
self.assertFalse( destination.exists() )
178+
self.assertEqual( source1.read_text(), "1" )
179+
self.assertEqual( source2.read_text(), "2" )
180+
181+
def testMissingSourceFile( self ) :
182+
183+
source = self.temporaryDirectory() / "missing"
184+
185+
rename = GafferDispatch.RenameFiles()
186+
rename["files"].setValue( IECore.StringVectorData( [ str( source ) ] ) )
187+
rename["name"].setValue( "renamed" )
188+
189+
with self.assertRaisesRegex( RuntimeError, f'.*(No such file|cannot find the file).*missing' ) :
190+
rename["task"].execute()
191+
192+
def testOneSourceOverwritingAnother( self ) :
193+
194+
fileA = self.temporaryDirectory() / "fileA"
195+
fileA.write_text( "A" )
196+
197+
fileB = self.temporaryDirectory() / "fileB"
198+
fileB.write_text( "B" )
199+
200+
rename = GafferDispatch.RenameFiles()
201+
rename["files"].setValue( IECore.StringVectorData( [ str( fileA ), str( fileB ) ] ) )
202+
rename["deleteSuffix"].setValue( "A" )
203+
rename["addSuffix"].setValue( "B" )
204+
205+
with self.assertRaisesRegex( RuntimeError, f'.*Renaming of ".*/{fileA.name}" would overwrite source ".*/{fileB.name}"' ) :
206+
rename["task"].execute()
207+
208+
self.assertEqual( fileA.read_text(), "A" )
209+
self.assertEqual( fileB.read_text(), "B" )
210+
211+
def testOverwriteExistingFile( self ) :
212+
213+
fileA = self.temporaryDirectory() / "fileA"
214+
fileA.write_text( "A" )
215+
216+
fileB = self.temporaryDirectory() / "fileB"
217+
fileB.write_text( "B" )
218+
219+
rename = GafferDispatch.RenameFiles()
220+
rename["files"].setValue( IECore.StringVectorData( [ str( fileA ) ] ) )
221+
rename["name"].setValue( "fileB" )
222+
223+
with self.assertRaisesRegex( RuntimeError, f'.*Can not overwrite destination ".*/{fileB.name}" unless `overwrite` plug is set.' ) :
224+
rename["task"].execute()
225+
226+
self.assertEqual( fileA.read_text(), "A" )
227+
self.assertEqual( fileB.read_text(), "B" )
228+
229+
rename["overwrite"].setValue( True )
230+
rename["task"].execute()
231+
232+
self.assertFalse( fileA.exists() )
233+
self.assertEqual( fileB.read_text(), "A" )
234+
235+
def testReplaceExtension( self ) :
236+
237+
file = self.temporaryDirectory() / "test.tiff"
238+
file.touch()
239+
240+
rename = GafferDispatch.RenameFiles()
241+
rename["files"].setValue( IECore.StringVectorData( [ str( file ) ] ) )
242+
rename["replaceExtension"].setValue( True )
243+
rename["extension"].setValue( "jpeg" )
244+
rename["task"].execute()
245+
246+
self.assertFalse( file.exists() )
247+
self.assertTrue( file.with_suffix( ".jpeg" ).exists() )
248+
249+
def testRemoveExtension( self ) :
250+
251+
file = self.temporaryDirectory() / "test.txt"
252+
file.touch()
253+
254+
rename = GafferDispatch.RenameFiles()
255+
rename["files"].setValue( IECore.StringVectorData( [ str( file ) ] ) )
256+
rename["replaceExtension"].setValue( True )
257+
rename["task"].execute()
258+
259+
self.assertFalse( file.exists() )
260+
self.assertTrue( file.with_suffix( "" ).exists() )
261+
262+
if __name__ == "__main__":
263+
unittest.main()

python/GafferDispatchTest/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@
6161
from .FileListTest import FileListTest
6262
from .DeleteFilesTest import DeleteFilesTest
6363
from .CopyFilesTest import CopyFilesTest
64+
from .RenameFilesTest import RenameFilesTest
6465

6566
if __name__ == "__main__":
6667
import unittest

0 commit comments

Comments
 (0)