Repository navigation
Expand file tree
/
Copy pathWindowsFileNameFix.sh
More file actions
executable file
·558 lines (472 loc) · 19.2 KB
/
Copy pathWindowsFileNameFix.sh
File metadata and controls
executable file
·558 lines (472 loc) · 19.2 KB
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
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
#!/bin/zsh
#
# ArchitectureScan
#
# by: Scott Kendall
#
# Written: 03/05/2026
# Last updated: 04/07/2026
#
# Script Purpose: Scan a finder directory (and sub-folders) to make sure all files are windows "safe"
#
# 1.0 - Initial
# 2.0 - Updated SD Version requirements to 3.1.0
# Added ability to set subtitle, color, and padding from defaults file
######################################################################################################
#
# Global "Common" variables
#
######################################################################################################
#set -x
SCRIPT_NAME="WindowsFileNameFix"
export PATH=/usr/bin:/bin:/usr/sbin:/sbin
LOGGED_IN_USER=$( scutil <<< "show State:/Users/ConsoleUser" | awk '/Name :/ && ! /loginwindow/ { print $3 }' )
USER_DIR=$( dscl . -read /Users/${LOGGED_IN_USER} NFSHomeDirectory | awk '{ print $2 }' )
USER_UID=$(id -u "$LOGGED_IN_USER")
FREE_DISK_SPACE=$(($( /usr/sbin/diskutil info / | /usr/bin/grep "Free Space" | /usr/bin/awk '{print $6}' | /usr/bin/cut -c 2- ) / 1024 / 1024 / 1024 ))
MACOS_NAME=$(sw_vers -productName)
MACOS_VERSION=$(sw_vers -productVersion)
MAC_RAM=$(($(sysctl -n hw.memsize) / 1024**3))" GB"
MAC_CPU=$(sysctl -n machdep.cpu.brand_string)
ICON_FILES="/System/Library/CoreServices/CoreTypes.bundle/Contents/Resources/"
# Swift Dialog version requirements
SW_DIALOG="/usr/local/bin/dialog"
MIN_SD_REQUIRED_VERSION="3.1.0"
[[ -e "${SW_DIALOG}" ]] && SD_VERSION=$( ${SW_DIALOG} --version) || SD_VERSION="0.0.0"
HOUR=$(date +%H)
case $HOUR in
0[0-9]|1[0-1]) GREET="morning" ;;
1[2-7]) GREET="afternoon" ;;
*) GREET="evening" ;;
esac
SD_DIALOG_GREETING="Good $GREET"
JSON_DIALOG_BLOB=$(mktemp "/var/tmp/${SCRIPT_NAME}_json.XXXXX")
DIALOG_COMMAND_FILE=$(mktemp "/var/tmp/${SCRIPT_NAME}_cmd.XXXXX")
TMP_FILE_STORAGE=$(mktemp "/var/tmp/${SCRIPT_NAME}_cmd.XXXXX")
chmod 666 $JSON_DIALOG_BLOB
chmod 666 $DIALOG_COMMAND_FILE
chmod 666 $TMP_FILE_STORAGE
###################################################
#
# App Specific variables (Feel free to change these)
#
###################################################
# See if there is a "defaults" file...if so, read in the contents
DEFAULTS_DIR="/Library/Managed Preferences/com.gianteaglescript.defaults.plist"
if [[ -f "$DEFAULTS_DIR" ]]; then
echo "Found Defaults Files. Reading in Info"
SUPPORT_DIR=$(defaults read "$DEFAULTS_DIR" SupportFiles)
SD_BANNER_IMAGE="${SUPPORT_DIR}$(defaults read "$DEFAULTS_DIR" BannerImage)"
BANNER_TEXT_PADDING=$(defaults read "$DEFAULTS_DIR" BannerPadding)
BANNER_SUBTITLE=$(defaults read "$DEFAULTS_DIR" BannerSubtitle)
BANNER_TEXT_COLOR=$(defaults read "$DEFAULTS_DIR" TitleFontColor)
else
SUPPORT_DIR="/Library/Application Support/GiantEagle"
SD_BANNER_IMAGE="${SUPPORT_DIR}/SupportFiles/GE_SD_BannerImage.png"
BANNER_TEXT_PADDING=10 #10 spaces to accommodate for icon offset
BANNER_SUBTITLE=""
fi
[[ -e $SUPPORT_DIR/$SD_BANNER_IMAGE ]] && SD_BANNER_IMAGE="$SUPPORT_DIR/$SD_BANNER_IMAGE"
[[ -z "$BANNER_TEXT_COLOR" ]] && BANNER_TEXT_COLOR="white"
# Log files location
LOG_FILE="${SUPPORT_DIR}/logs/${SCRIPT_NAME}.log"
# Display items (banner / icon)
SD_WINDOW_TITLE="Windows FileName Fix"
SD_ICON_FILE="https://files.softicons.com/download/business-icons/ecommerce-and-business-icons-by-designcontest.com/ico/bar-code.ico"
SD_OVERLAY_ICON="https://files.softicons.com/download/system-icons/colobrush-icons-by-eponas-deeway/ico/icone_windows.ico"
SUPPORT_FILE_INSTALL_POLICY="install_SymFiles"
DIALOG_INSTALL_POLICY="install_SwiftDialog"
JQ_FILE_INSTALL_POLICY="install_jq"
##################################################
#
# Passed in variables
#
#################################################
JAMF_LOGGED_IN_USER=${3:-"$LOGGED_IN_USER"} # Passed in by JAMF automatically
SD_FIRST_NAME="${(C)${JAMF_LOGGED_IN_USER%%.*}}"
TARGET_PATH=${4:-"$USER_DIR/Library/CloudStorage/OneDrive-GiantEagle,Inc"}
####################################################################################################
#
# Functions
#
####################################################################################################
function admin_user ()
{
[[ $UID -eq 0 ]] && return 0 || return 1
}
function create_log_directory ()
{
# Ensure that the log directory and the log files exist. If they
# do not then create them and set the permissions.
#
# RETURN: None
# If the log directory doesn't exist - create it and set the permissions (using zsh parameter expansion to get directory)
if admin_user; then
LOG_DIR=${LOG_FILE%/*}
[[ ! -d "${LOG_DIR}" ]] && /bin/mkdir -p "${LOG_DIR}"
/bin/chmod 755 "${LOG_DIR}"
# If the log file does not exist - create it and set the permissions
[[ ! -f "${LOG_FILE}" ]] && /usr/bin/touch "${LOG_FILE}"
/bin/chmod 644 "${LOG_FILE}"
fi
}
function logMe ()
{
# Basic two pronged logging function that will log like this:
#
# 20231204 12:00:00: Some message here
#
# This function logs both to STDOUT/STDERR and a file
# The log file is set by the $LOG_FILE variable.
# if the user is an admin, it will write to the logfile, otherwise it will just echo to the screen
#
# RETURN: None
if admin_user; then
echo "$(/bin/date '+%Y-%m-%d %H:%M:%S'): ${1}" | tee -a "${LOG_FILE}"
else
echo "$(/bin/date '+%Y-%m-%d %H:%M:%S'): ${1}"
fi
}
function check_for_sudo ()
{
# Ensures that script is run as ROOT
if ! admin_user; then
MainDialogBody=(
--message "**Admin access required!**<br><br>In order for this script to function properly, it must be run as an admin user!"
--ontop
--icon "${SD_ICON_FILE}"
--overlayicon warning
--bannerimage "${SD_BANNER_IMAGE}"
--bannertitle "${SD_WINDOW_TITLE}"
--subtitle "$BANNER_SUBTITLE"
--width 700
--titlefont shadow=1,color="${BANNER_TEXT_COLOR}",offset="${BANNER_TEXT_PADDING}"
--button1text "OK"
)
"${SW_DIALOG}" "${MainDialogBody[@]}" 2>/dev/null
cleanup_and_exit 1
fi
}
function check_swift_dialog_install ()
{
# Check to make sure that Swift Dialog is installed and functioning correctly
# Will install process if missing or corrupted
#
# RETURN: None
logMe "Ensuring that swiftDialog version is installed..."
if [[ ! -x "${SW_DIALOG}" ]]; then
logMe "Swift Dialog is missing or corrupted - Installing from JAMF"
install_swift_dialog
fi
SD_VERSION=$( ${SW_DIALOG} --version)
if ! is-at-least "${MIN_SD_REQUIRED_VERSION}" "${SD_VERSION}"; then
logMe "Swift Dialog is outdated - Installing version '${MIN_SD_REQUIRED_VERSION}' from JAMF..."
install_swift_dialog
else
logMe "Swift Dialog is currently running: ${SD_VERSION}"
fi
}
function install_swift_dialog ()
{
# Install Swift dialog From JAMF
# PARMS Expected: DIALOG_INSTALL_POLICY - policy trigger from JAMF
#
# RETURN: None
/usr/local/bin/jamf policy -event ${DIALOG_INSTALL_POLICY}
}
function check_support_files ()
{
[[ ! -e "${SD_BANNER_IMAGE}" ]] && [[ "${SD_BANNER_IMAGE}" =~ \.(jpg|png|heic)$ ]] && /usr/local/bin/jamf policy -event ${SUPPORT_FILE_INSTALL_POLICY}
[[ $(which jq) == *"not found"* ]] && /usr/local/bin/jamf policy -event ${JQ_INSTALL_POLICY}
}
function create_infobox_message()
{
################################
#
# Swift Dialog InfoBox message construct
#
################################
SD_INFO_BOX_MSG="## System Info ##<br>"
SD_INFO_BOX_MSG+="${MAC_CPU}<br>"
SD_INFO_BOX_MSG+="{serialnumber}<br>"
SD_INFO_BOX_MSG+="${MAC_RAM} RAM<br>"
SD_INFO_BOX_MSG+="${FREE_DISK_SPACE}GB Available<br>"
SD_INFO_BOX_MSG+="${MACOS_NAME} ${MACOS_VERSION}<br>"
}
function check_logged_in_user ()
{
# PURPOSE: Make sure there is a logged in user
# RETURN: None
# EXPECTED: $LOGGED_IN_USER
if [[ -z "$LOGGED_IN_USER" ]] || [[ "$LOGGED_IN_USER" == "loginwindow" ]]; then
logMe "INFO: No user logged in, exiting"
cleanup_and_exit 0
else
logMe "INFO: User $LOGGED_IN_USER is logged in"
fi
}
function cleanup_and_exit ()
{
[[ -f ${JSON_OPTIONS} ]] && /bin/rm -rf ${JSON_OPTIONS}
[[ -f ${TMP_FILE_STORAGE} ]] && /bin/rm -rf ${TMP_FILE_STORAGE}
[[ -f ${DIALOG_COMMAND_FILE} ]] && /bin/rm -rf ${DIALOG_COMMAND_FILE}
exit $1
}
function construct_dialog_header_settings ()
{
# Construct the basic Swift Dialog screen info that is used on all messages
#
# RETURN: None
# VARIABLES expected: All of the Widow variables should be set
# PARMS Passed: $1 is message to be displayed on the window
echo '{
"icon" : "'${SD_ICON_FILE}'",
"message" : "'$1'",
"bannerimage" : "'${SD_BANNER_IMAGE}'",
"infobox" : "'${SD_INFO_BOX_MSG}'",
"overlayicon" : "'${SD_OVERLAY_ICON}'",
"ontop" : "true",
"bannertitle" : "'${SD_WINDOW_TITLE}'",
"subtitle" : "'${BANNER_SUBTITLE}'",
"titlefont" : "shadow=1,color='${BANNER_TEXT_COLOR}',offset='${BANNER_TEXT_PADDING}'",
"button1text" : "Wait",
"button2text" : "OK",
"moveable" : "true",
"height" : "80%",
"width" : "900",
"json" : "true",
"ignorednd" : "true",
"quitkey" : "0",'
}
function create_listitem_list ()
{
# PURPOSE: Create the display list for the dialog box
# RETURN: None
# EXPECTED: JSON_DIALOG_BLOB should be defined
# PARMS: $1 - message to be displayed on the window
# $2 - type of data to parse XML or JSON
# #3 - key to parse for list items
# $4 - string to parse for list items
# EXPECTED: None
construct_dialog_header_settings $1 > "${JSON_DIALOG_BLOB}"
create_listitem_message_body "" "" "" "" "" "first"
for item in "${APP_LIST[@]}"; do
name=${item:t}
create_listitem_message_body "$name" "" "" "Pending..." "pending" ""
done
create_listitem_message_body "" "" "" "" "" "last"
update_display_list "Create"
}
function create_listitem_message_body ()
{
# PURPOSE: Construct the List item body of the dialog box
# "listitem" : [
# {"title" : "macOS Version:", "icon" : "/System/Library/CoreServices/CoreTypes.bundle/Contents/Resources/FinderIcon.icns", "status" : "${macOS_version_icon}", "statustext" : "$sw_vers"},
# RETURN: None
# EXPECTED: message
# PARMS: $1 - title
# $2 - icon
# $3 - status text (for display)
# $4 - status
# $5 - first or last - construct appropriate listitem heders / footers
declare line && line=""
[[ "$6:l" == "first" ]] && line+='"button1disabled" : "true", "listitem" : ['
[[ ! -z $1 ]] && [[ ! -z $2 ]] && line+='{"title" : "'$1'", "subtitle" : "'$2'", "icon" : "'$3'", "status" : "'$5'", "statustext" : "'$4'"},'
[[ ! -z $1 ]] && [[ -z $2 ]] && line+='{"title" : "'$1'", "icon" : "'$3'", "status" : "'$5'", "statustext" : "'$4'"},'
[[ "$6:l" == "last" ]] && line+=']}'
echo $line >> ${JSON_DIALOG_BLOB}
}
function update_display_list ()
{
# setopt -s nocasematch
# This function updates the Swift Dialog list display with easy to implement parameter passing...
# The Swift Dialog native structure is very strict with the command structure...this routine makes
# it easier to implement
#
# Param list
#
# $1 - Action to be done ("Create", "Add", "Change", "Clear", "Info", "Show", "Done", "Update")
# ${2} - Affected item (2nd field in JSON Blob listitem entry)
# ${3} - Icon status "wait, success, fail, error, pending or progress"
# ${4} - Status Text
# $5 - Progress Text (shown below progress bar)
# $6 - Progress amount
# increment - increments the progress by one
# reset - resets the progress bar to 0
# complete - maxes out the progress bar
# If an integer value is sent, this will move the progress bar to that value of steps
# the GLOB :l converts any inconing parameter into lowercase
case "${1:l}" in
"create" | "show" )
# Display the Dialog prompt
$SW_DIALOG --progress --jsonfile "${JSON_DIALOG_BLOB}" --commandfile "${DIALOG_COMMAND_FILE}" & sleep .2
DIALOG_PROCESS=$! #Grab the process ID of the background process
;;
"buttondisable" )
# disable button 1
/bin/echo "button1: disable" >> "${DIALOG_COMMAND_FILE}"
;;
"buttonenable" )
# Enable button 1
/bin/echo "button1: enable" >> "${DIALOG_COMMAND_FILE}"
;;
"buttonchange" )
# change text of button 1
/bin/echo "button1text: ${2}" >> "${DIALOG_COMMAND_FILE}"
;;
"update" | "change" )
#
# Increment the progress bar by ${2} amount
#
# change the list item status and increment the progress bar
/bin/echo "listitem: title: ${3}, status: ${5}, statustext: ${4}" >> "${DIALOG_COMMAND_FILE}"
/bin/echo "progress: ${6}" >> "${DIALOG_COMMAND_FILE}"
/bin/sleep .5
;;
"progress" )
# Increment the progress bar by static amount ($6)
# Display the progress bar text ($5)
/bin/echo "progress: ${6}" >> "${DIALOG_COMMAND_FILE}"
/bin/echo "progresstext: ${5}" >> "${DIALOG_COMMAND_FILE}"
;;
"createlist" )
# Construct a list with the names of the files that were discovered
echo "list: " >> "${DIALOG_COMMAND_FILE}"
echo "listitem: delete, index: 0" >> ${DIALOG_COMMAND_FILE}
;;
esac
}
####################################################################################################
#
# Script Specific Functions
#
####################################################################################################
function welcomemsg ()
{
message="$SD_DIALOG_GREETING $SD_FIRST_NAME. "
message+="When you save files to the cloud (like **OneDrive**), Windows has specific rules for how they can be named. To make sure your files sync correctly and don't get lost, some corrections need to be made."
message+="<br><br>Starting scan at: **$TARGET_PATH**<br> "
preload_apps
APP_LIST=("${reply[@]}")
construct_dialog_header_settings $message > "${JSON_DIALOG_BLOB}"
echo "}" >> "${JSON_DIALOG_BLOB}"
update_display_list "Create"
update_display_list "buttondisable"
# show the applications in a list
populate_list
# 2nd pass which actually changes the names
scan_apps
update_display_list "progress" "" "" "" "" 100
[[ $writeLog == "yes" ]] && update_display_list "buttonchange" "Export" || update_display_list "buttonchange" "Done"
update_display_list "buttonenable"
wait
buttonpress=$?
if [[ $writeLog == "yes" ]] && [[ $buttonpress == 0 ]]; then
export_failed_items
fi
}
function preload_apps ()
{
find "$TARGET_PATH" -depth -not -path '*/.*' | while read -r app; do
[[ -n "$EXCLUSION_LIST" && "${app}" =~ "$EXCLUSION_LIST" ]] && continue
reply+=("$app")
done
APPLIST_COUNT=$#reply
}
function scan_apps ()
{
local writeLog="no"
local dir_name base_name new_name check_res appStatus final_path path_length relative_path scanned
echo "Old File","New File","Reason" >> "${TMP_FILE_STORAGE}"
find "$TARGET_PATH" -depth -not -path '*/.*' -print0 | while read -r -d '' old_path; do
# Set some default values
appStatus="Passed"
statusIcon="success"
dir_name=$(dirname "$old_path")
base_name=$(basename "$old_path")
display_name="${old_path:h:t}/${old_path:t}"
[[ -n "$EXCLUSION_LIST" && "${old_path}" =~ "$EXCLUSION_LIST" ]] && continue
((scanned++))
# 1. Replace Illegal Characters (< > : " / \ | ? *)
new_name=$(echo "$base_name" | sed 's/[<>:"\\|?*]/_/g')
# 2. Scrub Trailing Periods and Spaces (Windows ignores these and breaks)
new_name=$(echo "$new_name" | sed 's/[. ]*$//')
# 3. Check Reserved Names
check_res="${new_name%.*}"
for res in "${reserved_names[@]}"; do
if [[ "${check_res:u}" == "$res" ]]; then
new_name="_${new_name}"
appStatus="Reserved name"
statusIcon="error"
break
fi
done
if [[ "$base_name" != "$new_name" && "$appStatus" != ("success"|"Reserved name") ]]; then
appStatus="Illegal characters"
statusIcon="error"
fi
# --- PATH STABILITY LOGIC ---
final_path="$dir_name/$new_name"
# 4. Check for max length
# Strip the starting directory from the front of the path
# The '#' operator removes the smallest matching prefix
relative_path="${old_path#$TARGET_PATH}"
# Remove leading slash if it remains after stripping
relative_path="${relative_path#/}"
# Calculate length of the remaining path
path_length=${#relative_path}
if [[ $path_length -ge 255 ]]; then
appStatus="Path to Long"
statusIcon="error"
fi
if [[ "$appStatus" != "Passed" ]]; then
# Collision Check: Does the target name already exist?
# On Mac, "File.txt" and "file.txt" can coexist; on Windows, they CANNOT.
if [[ -e "$final_path" ]]; then
extension="${new_name##*.}"
filename="${new_name%.*}"
# Append a timestamp to make it unique
new_name="${filename}_$(date +%s).${extension}"
final_path="$dir_name/$new_name"
fi
writeLog="yes"
# Write to CSV log
echo $old_path,$final_path,$appStatus >> "${TMP_FILE_STORAGE}"
mv "${old_path}" "${final_path}"
logMe "Changed name to: ${final_path}"
fi
echo "listitem: title: $display_name, subtitle: $new_name, status: $statusIcon, statustext: $appStatus" >> "${DIALOG_COMMAND_FILE}"
update_display_list "progress" "" "" "" "" $((100*scanned/APPLIST_COUNT))
done
}
function populate_list ()
{
update_display_list "createlist"
for item in ${APP_LIST[@]}; do
fileName="${item:h:t}/${item:t}"
echo "listitem: add, title: ${fileName}, subtitle: ${item:t}, status: pending", statustext: pending >> "${DIALOG_COMMAND_FILE}"
done
}
function export_failed_items ()
{
logMe "Changed files list stored in $EXPORTED_LIST"
mv "${TMP_FILE_STORAGE}" $EXPORTED_LIST
}
####################################################################################################
#
# Main Script
#
####################################################################################################
local APPLIST_COUNT=$(find "$TARGET_PATH" -not -path '*/.*' | wc -l | xargs)
local -a APP_LIST
local reserved_names=(CON PRN AUX NUL COM0 COM1 COM2 COM3 COM4 COM5 COM6 COM7 COM8 COM9 LPT0 LPT1 LPT2 LPT3 LPT4 LPT5 LPT6 LPT7 LPT8 LPT9)
local EXCLUSION_LIST="sparsebundle|Icon"
local EXPORTED_LIST="$USER_DIR/Desktop/ChangedFiles.csv"
autoload 'is-at-least'
check_for_sudo
create_log_directory
check_swift_dialog_install
check_support_files
create_infobox_message
welcomemsg
cleanup_and_exit 0