An FTP server update script

Good day everyone!
Mark Webber is on pole position for the third time this season and I finished my FTP update script this morning, so today is definitely a wonderful day.

But of course this is not the forum to talk about formula 1.

Some time ago I started learning HTML, CSS, PHP and JavaScript to build my site with my own code and it’s going pretty well.

Before I coded the webpages myself I used RapidWeaver to build them. The feature I liked the most was the updating aspect of it. It only updated the pages I had edited and in that way reduced the time it took to upload all webpages. Now that I’m writing the webpages myself, I of course don’t use RapidWeaver anymore. Before this script I had to remember which pages I had edited and then I had to upload them one by one with Cyberduck. I got sick of it and since every Mac comes with AppleScript, I thought of making a script which only updates the files I edited.

The script only works with FTP servers and uses cURL to upload files and make directories. To use this script, you’ll also need the web functions I made and I script file which saves the modification date of the updated files.

An almost ready-to-use version is available at http://developerief2.site11.com/Public-Files/Site_Update.zip. The only thing you’ll have to do to make it work, is open it with ScriptEditor and change the properties in the beginning of the script.

Site Uploader.app/Contents/Resources/Scripts/main.scpt

-- ================================================
-- =================== DEFAULTS 
-- ================================================
property SITE_FTP_IP_ADDRESS : "1.1.1.1"
property SITE_FTP_USERNAME : "username"
property SITE_FTP_ROOT_DIRECTORY : "1.1.1.1/public_html/"

property HTML_FOLDER : missing value
property FILE_INDEX_SCRIPT : ((path to me as text) & "Contents:Resources:file-index.scpt") as alias

property DEST_FOLDER : "/"

property LOG_FILE : ((path to me as text) & "Contents:Resources:log.txt") as string

-- ================================================
-- =================== GLOBALS 
-- ================================================
global _loadedScript
global _sitePassword
global _errorCount
global _updatedCount
global _silenceMode

-- ================================================
-- =================== MAIN 
-- ================================================
on run
	set HTML_FOLDER to CheckValidFolder(HTML_FOLDER, "Choose the folder containing the HTML source files")
	
	set _errorCount to 0
	set _updatedCount to 0
	
	set _sitePassword to AskForPassword("The password of the FTP server is needed")
	set _silenceMode to (button returned of (display dialog "Would you like to run in silence mode?" buttons {"Yes", "No"} default button 2)) is "Yes"
	
	set _loadedScript to LoadFileIndex()
	Main(HTML_FOLDER, DEST_FOLDER)
	SaveFileIndex(_loadedScript)
	
	if _silenceMode is false then display alert ((_errorCount & " errors have occurred." & return & _updatedCount & " files have been updated.") as string)
end run

-- ================================================
-- =================== HANDLERS 
-- ================================================
on Main(srcFolder)
	set srcFolder to srcFolder as alias
	set WebFunctions to LoadWebFunctions()
	
	-- get entire contents
	set entireFolderContents to list folder srcFolder
	
	-- loop to folder contents
	repeat with anItemName in entireFolderContents
		
		-- construct full path
		set anItem to ((srcFolder as text) & anItemName) as alias
		
		-- get info
		set itemInfo to (info for anItem)
		
		-- construct destination folder path
		set sourceFilePathComponents to PathComponents(POSIX path of anItem)
		set sourceFolderPathComponents to PathComponents(POSIX path of HTML_FOLDER)
		set pathComponentsBetween to ((count sourceFilePathComponents) - (count sourceFolderPathComponents) - 1)
		
		set destFolder to DEST_FOLDER
		set componentsToAdd to {}
		if pathComponentsBetween is not 0 then
			set componentsToAdd to items ((count sourceFolderPathComponents) + 1) thru (((count sourceFolderPathComponents) + 1) + (pathComponentsBetween - 1)) of sourceFilePathComponents
			repeat with i in componentsToAdd
				set destFolder to StringByAppendingPathComponent(POSIX path of destFolder, (i as string))
			end repeat
		end if
		destFolder
		if destFolder is not "/" then WebFunctions's MakeFolderOnFTPServer(SITE_FTP_IP_ADDRESS, SITE_FTP_USERNAME, _sitePassword, ("/public_html" & destFolder) as string)
		
		
		-- if a file and visible
		if (visible of itemInfo) and not (folder of itemInfo) then
			-- get last edited of list
			set lastOploadedEdit to (GetEditDateOfFile_InList_(POSIX path of anItem, _loadedScript's l))
			set lastGlobalEdit to (modification date of itemInfo) as date
			
			-- if updated
			if (lastOploadedEdit is missing value) or (lastOploadedEdit < lastGlobalEdit) then
				-- construct upload URL
				set uploadPath to ("ftp://" & SITE_FTP_ROOT_DIRECTORY) as string
				repeat with i in componentsToAdd
					set uploadPath to StringByAppendingPathComponent(uploadPath, i as string)
				end repeat
				set uploadPath to StringByAppendingPathComponent(uploadPath, name of itemInfo)
				
				if _silenceMode is false then display dialog ("Uploading " & name of itemInfo & return & return & quoted form of (POSIX path of anItem)) as string buttons "OK" default button 1 giving up after 3
				set uploadSuccess to (WebFunctions's uploadFile(POSIX path of anItem, uploadPath, SITE_FTP_USERNAME, _sitePassword))
				if uploadSuccess then
					LogToFile(quoted form of (POSIX path of anItem) & " successfully uploaded.")
					SetEditDate_OfFile_(lastGlobalEdit, POSIX path of anItem)
					SaveFileIndex(_loadedScript)
					set _updatedCount to _updatedCount + 1
				else
					set _errorCount to _errorCount + 1
					LogToFile("** ERROR **: " & quoted form of (POSIX path of anItem) & " could not be uploaded.")
				end if
			end if
		end if
		
		-- if a folder
		if (folder of itemInfo) then
			Main(anItem)
		end if
	end repeat
end Main

on CheckValidFolder(aHSF, prmpt)
	try
		get aHSF as alias
		return aHSF as alias
	on error
		set newFolder to (choose folder with prompt prmpt)
		return newFolder
	end try
end CheckValidFolder

on LoadFileIndex()
	load script FILE_INDEX_SCRIPT
	return result
end LoadFileIndex

on SaveFileIndex(scr)
	store script scr in FILE_INDEX_SCRIPT with replacing
end SaveFileIndex

on LoadWebFunctions()
	set scriptFile to (path to me as text) & "Contents:Resources:Scripts:Web Functions.scpt"
	set scriptFile to scriptFile as alias
	return load script scriptFile
end LoadWebFunctions

on GetEditDateOfFile_InList_(fileName, fileList)
	repeat with i in fileList
		if (name of i) is fileName then
			return date (contents of (contents of i))
		end if
	end repeat
	
	return missing value
end GetEditDateOfFile_InList_

on SetEditDate_OfFile_(newDate, fileName)
	repeat with i in (_loadedScript's l)
		if name of i is fileName then
			set contents of (contents of i) to (newDate as string)
			return
		end if
	end repeat
	
	set end of (_loadedScript's l) to {name:fileName, contents:(newDate as string)}
end SetEditDate_OfFile_

on CopyFile(sourceFile, destFolder)
	do shell script "cp " & quoted form of (POSIX path of sourceFile) & space & quoted form of (POSIX path of destFolder)
end CopyFile

on PathComponents(fileName)
	set tid to AppleScript's text item delimiters
	set AppleScript's text item delimiters to "/"
	set components to every text item of fileName
	set AppleScript's text item delimiters to ""
	if fileName starts with "/" then set item 1 of components to "/"
	
	-- if end with slash
	if item -1 of components is "" then set components to (items 1 thru -2 of components)
	
	set AppleScript's text item delimiters to tid
	return components
end PathComponents

on PathWithComponents(array)
	-- if empty
	if (count array) is 0 then return ""
	
	-- if slash
	if (count array) is 1 and (item 1 of array) is "/" then return "/"
	
	set tid to AppleScript's text item delimiters
	set AppleScript's text item delimiters to "/"
	set newPath to array as text
	set AppleScript's text item delimiters to ""
	
	if (item 1 of array) is "/" then set newPath to (characters 2 thru -1 of newPath) as string
	
	set AppleScript's text item delimiters to tid
	return newPath
end PathWithComponents

on StringByAppendingPathComponent(original, addition)
	if original is not "" then if original does not end with "/" then set original to (original & "/") as string
	set newString to (original & addition) as string
	return newString
end StringByAppendingPathComponent

on FileExistsAtPath(aHSF)
	try
		get aHSF as alias
		log "file \"" & aHSF & " exists"
		return true
	on error
		log "file \"" & aHSF & " does not exist"
		return false
	end try
end FileExistsAtPath

on MakeFolder(aHSF)
	try
		do shell script "mkdir " & quoted form of (POSIX path of aHSF)
	end try
	return (aHSF as alias)
end MakeFolder


on LogToFile(errorString)
	set myDate to do shell script "date \"+%d-%m-%Y || %H:%M:%S\""
	set myData to (return & myDate & ": " & errorString) as string
	
	if not FileExistsAtPath(LOG_FILE) then MakeFile(POSIX path of LOG_FILE, "1")
	
	if FileExistsAtPath(LOG_FILE) then
		set logf to LOG_FILE as alias
		
		set OA to open for access logf with write permission
		try
			write myData to OA starting at eof
			close access OA
		on error
			try
				close access OA
			end try
		end try
	end if
end LogToFile

on AskForPassword(prmpt)
	set pass1 to text returned of (display dialog prmpt default answer "" with hidden answer)
	set pass2 to text returned of (display dialog "Verify you password" default answer "" with hidden answer)
	
	if pass1 is not pass2 then set pass1 to AskForPassword("The given passwords do not match")
	
	return pass1
end AskForPassword

on stringForTerminal(fileName)
	set tid to AppleScript's text item delimiters
	set AppleScript's text item delimiters to " "
	set allItems to every text item of fileName
	set AppleScript's text item delimiters to "\\ "
	set newPath to allItems as text
	
	set AppleScript's text item delimiters to tid
	return newPath
end stringForTerminal

on MakeFile(fileName, tsize)
	do shell script "mkfile " & tsize & space & my stringForTerminal(fileName)
end MakeFile

Site Uploader.app/Contents/Resources/Scripts/web functions.scpt


on sourceOfWebPage(anURL)
	set theSource to missing value
	
	try
		set theSource to do shell script "curl " & quoted form of anURL
	end try
	
	return theSource
end sourceOfWebPage

on MakeFolderOnFTPServer(serverAddress, sUsername, sPassword, dirCreation)
	set allComponents to pathComponents(dirCreation)
	
	repeat with i from 1 to (count allComponents)
		set myPath to pathWithComponents(items 1 thru i of allComponents)
		set myPath to stringForTerminal(myPath)
		
		set myCommand to ("curl ftp://" & sUsername & ":" & sPassword & "@" & serverAddress) as string
		set myCommand to (myCommand & " -Q \"MKD " & myPath & "\"") as string
		
		try
			do shell script myCommand
		end try
	end repeat
end MakeFolderOnFTPServer

on uploadFile(filename, anURL, userName, aPassword)
	set anURL to my encodeTextForURL(anURL, false, false)
	log anURL
	set myName to "curl "
	set myParam to "-T "
	
	
	-- upload type
	set tid to AppleScript's text item delimiters
	set AppleScript's text item delimiters to "://"
	try
		set myType to text item 1 of anURL
		set AppleScript's text item delimiters to tid
	on error e
		log "error 1: " & e
		set AppleScript's text item delimiters to tid
		return false
	end try
	
	-- upload path
	try
		set AppleScript's text item delimiters to "://"
		set urlPath to text item 2 of anURL
		set AppleScript's text item delimiters to tid
	on error e
		log "error 2: " & e
		set AppleScript's text item delimiters to tid
		return false
	end try
	
	-- full url
	set AppleScript's text item delimiters to ""
	set fullURLPath to (myType & "://" & userName & ":" & aPassword & "@" & urlPath) as text
	set AppleScript's text item delimiters to tid
	
	set fullCMD to (myName & myParam & quoted form of filename & " " & quoted form of fullURLPath) as string
	
	-- upload
	try
		do shell script fullCMD
		log fullCMD
		return true
	on error e
		log "error 3: " & e
		return false
	end try
end uploadFile

on encodeTextForURL(this_text, encode_URL_A, encode_URL_B)
	set the standard_characters to "abcdefghijklmnopqrstuvwxyz0123456789"
	set the URL_A_chars to "$+!'/?;&@=#%><{}[]\"~`^\\|*"
	set the URL_B_chars to ".-_:"
	set the acceptable_characters to the standard_characters
	if encode_URL_A is false then set the acceptable_characters to the acceptable_characters & the URL_A_chars
	if encode_URL_B is false then set the acceptable_characters to the acceptable_characters & the URL_B_chars
	set the encoded_text to ""
	repeat with this_char in this_text
		if this_char is in the acceptable_characters then
			set the encoded_text to (the encoded_text & this_char)
		else
			set the encoded_text to (the encoded_text & encode_char(this_char)) as string
		end if
	end repeat
	return the encoded_text
end encodeTextForURL


(* ****** === OTHER === ****** *)
on pathComponents(filename)
	set tid to AppleScript's text item delimiters
	set AppleScript's text item delimiters to "/"
	set components to every text item of filename
	set AppleScript's text item delimiters to ""
	if filename starts with "/" then set item 1 of components to "/"
	
	-- if end with slash
	if item -1 of components is "" then set components to (items 1 thru -2 of components)
	
	set AppleScript's text item delimiters to tid
	return components
end pathComponents

on stringForTerminal(filename)
	set tid to AppleScript's text item delimiters
	set AppleScript's text item delimiters to " "
	set allItems to every text item of filename
	set AppleScript's text item delimiters to "\\ "
	set newPath to allItems as text
	
	set AppleScript's text item delimiters to tid
	return newPath
end stringForTerminal

on pathWithComponents(array)
	-- if empty
	if (count array) is 0 then return ""
	
	-- if slash
	if (count array) is 1 and (item 1 of array) is "/" then return "/"
	
	set tid to AppleScript's text item delimiters
	set AppleScript's text item delimiters to "/"
	set newPath to array as text
	set AppleScript's text item delimiters to ""
	
	if (item 1 of array) is "/" then set newPath to (characters 2 thru -1 of newPath) as string
	
	set AppleScript's text item delimiters to tid
	return newPath
end pathWithComponents

Site Uploader.app/Contents/Resources/file-index.scpt

property l : {}

Those are all the scripts that are present in the application bundle. You can of course also save the script in a script bundle.

The only thing that bothers me is the time it takes to loop thru the entire contents of the HTML folder and then loop thru all stored modification dates. If someone knows a better way to loop thru the modification dates, please share it.

I hope this script can be useful to someone,
ief2