Transforming files with msdeploy - msdeploy

Can I use the config transforms mechanism of MSDeploy to transform other files?

(another approach)
The msdeploy packaging is jsut invoked during an MSbuild run for your project.
TransformXml is an included task of a .csproj or .vsproj build.
Just modify your build process to invoke that task on whatever file you need.
For example, what we do is write a custom target
<Target Name="TransformFile">
<TransformXml Source="$(DestinationPath)\$(Sourcefile)"
Transform="$(DestinationPath)\$(TransformFile)"
Destination="$(DestinationPath)\$(DestFile)" />
</Target>
Then modify your .csproj to run this BEFORE the Publish task is invoked.
<CallTarget Targets="TransformFile"
Condition="'$(CustomTransforms)'=='true'" />

The answer by Taylor didn't work for me and he didn't provide further details. So I went spelunking into the Microsoft.Web.Publishing.targets file to find a solution. The following MSBuild Target can be added to project file to transform all other config files in the root application directory. Enjoy :)
<Target Name="TransformOtherConfigs" AfterTargets="CollectWebConfigsToTransform">
<ItemGroup>
<WebConfigsToTransform Include="#(FilesForPackagingFromProject)"
Condition="'%(FilesForPackagingFromProject.Extension)'=='.config'"
Exclude="*.$(Configuration).config;$(ProjectConfigFileName)">
<TransformFile>%(RelativeDir)%(Filename).$(Configuration).config</TransformFile>
<TransformOriginalFile>$(TransformWebConfigIntermediateLocation)\original\%(DestinationRelativePath)</TransformOriginalFile>
<TransformOutputFile>$(TransformWebConfigIntermediateLocation)\transformed\%(DestinationRelativePath)</TransformOutputFile>
<TransformScope>$([System.IO.Path]::GetFullPath($(_PackageTempDir)\%(DestinationRelativePath)))</TransformScope>
</WebConfigsToTransform>
<WebConfigsToTransformOuputs Include="#(WebConfigsToTransform->'%(TransformOutputFile)')" />
</ItemGroup>
</Target>

Short answer: Yes you can. But it's "difficult".
Long answer:
When we deploy sites to destinations we had the usual web.test.config, and web.prod.config. This worked fine until we introduced log4net.test.config and log4net.prod.config. MSBuild will not automatically go through and replace all of these. It will only do the web.config ones.
If you want the nitty gritty go to the last code snippet. It shows the functions to take one config and replace it with a replacement. But... it will make more sense if I describe the whole process.
The process:
Msbuild makes a zip file package of the site.
We wrote a custom .net app that will take that zip file and do the config replacements on each one of the files. Resave the zip file.
Execute the msdeploy command to deploy the packaged file.
MSbuild will not automatically replace all of the extra configs. What's interesting is MSBuild will remove any "extra" configs. So your log4net.test.config will be gone after it's build. So the first thing you have to do is tell msdbuild to keep those extra files in place.
You have to modify your vbProj file to include a new setting:
<AutoParameterizationWebConfigConnectionStrings>False</AutoParameterizationWebConfigConnectionStrings>
Open your vbProj file for the web application into your favorite text editor. Navigate to each deploy configuration you want this to apply too (release, prod, debug, etc.) and add that config into it. Here is an example of our "release" config.
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
...
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<DebugType>pdbonly</DebugType>
<DefineDebug>false</DefineDebug>
<DefineTrace>true</DefineTrace>
<Optimize>true</Optimize>
<OutputPath>bin\</OutputPath>
<DocumentationFile>Documentation.xml</DocumentationFile>
<NoWarn>42016,41999,42017,42018,42019,42032,42036,42020,42021,42022,42353,42354,42355</NoWarn>
<CodeAnalysisRuleSet>AllRules.ruleset</CodeAnalysisRuleSet>
<DeployIisAppPath>IISAppPath</DeployIisAppPath>
<AutoParameterizationWebConfigConnectionStrings>False</AutoParameterizationWebConfigConnectionStrings>
</PropertyGroup>
...
</Project>
So now msbduild will build the project and keep those extra files in place and not do the replacements. Now you have to manually do them.
We wrote a .net app that will watch for these new zip files. I wrote some code that will spin through the whole zip package and find any configs that match the {configname}.{env}.config. It will extract them, replace them, and put them back. To do the actual replacement we use the same DLL's that MSDeploy uses. I also use Ionic.Zip to do the zip stuff.
So add reference to:
Microsoft.Build.dll
Microsoft.Build.Engine.dll
Microsoft.Web.Publishing.Tasks (possibly, not sure if you need this or not)
Import:
Imports System.IO
Imports System.Text.RegularExpressions
Imports Microsoft.Build.BuildEngine
Imports Microsoft.Build
Here is the code that spins through the zip file
specificpackage = "mypackagedsite.zip"
configenvironment = "DEV" 'stupid i had to pass this in, but it's the environment in web.dev.config
Directory.CreateDirectory(tempdir)
Dim fi As New FileInfo(specificpackage)
'copy zip file to temp dir
Dim tempzip As String = tempdir & fi.Name
File.Copy(specificpackage, tempzip)
''extract configs to merge from file into temp dir
'regex for the web.config
'regex for the web.env.config
'(?<site>\w+)\.(?<env>\w+)\.config$
Dim strMainConfigRegex As String = "/(?<configtype>\w+)\.config$"
Dim strsubconfigregex As String = "(?<site>\w+)\.(?<env>\w+)\.config$"
Dim strsubconfigregex2 As String = "(?<site>\w+)\.(?<env>\w+)\.config2$"
Dim MainConfigRegex As New Regex(strMainConfigRegex, RegexOptions.Compiled Or RegexOptions.IgnoreCase)
Dim SubConfigRegex As New Regex(strsubconfigregex, RegexOptions.Compiled Or RegexOptions.IgnoreCase)
Dim SubConfigRegex2 As New Regex(strsubconfigregex2, RegexOptions.Compiled Or RegexOptions.IgnoreCase)
Dim filetoadd As New Dictionary(Of String, String)
Dim filestoremove As New List(Of ZipEntry)
Using zip As ZipFile = ZipFile.Read(tempzip)
For Each entry As ZipEntry In From a In zip.Entries Where a.IsDirectory = False
For Each myMatch As Match In MainConfigRegex.Matches(entry.FileName)
If myMatch.Success Then
'found main config.
're-loop through, find any that are in the same dir as this, and also match the config name
Dim currentdir As String = Path.GetDirectoryName(entry.FileName)
Dim conifgmatchname As String = myMatch.Groups.Item("configtype").Value
For Each subentry In From b In zip.Entries Where b.IsDirectory = False _
And UCase(Path.GetDirectoryName(b.FileName)) = UCase(currentdir) _
And (UCase(Path.GetFileName(b.FileName)) = UCase(conifgmatchname & "." & configenvironment & ".config") Or
UCase(Path.GetFileName(b.FileName)) = UCase(conifgmatchname & "." & configenvironment & ".config2"))
entry.Extract(tempdir)
subentry.Extract(tempdir)
'Go ahead and do the transormation on these configs
Dim newtransform As New doTransform
newtransform.tempdir = tempdir
newtransform.filename = entry.FileName
newtransform.subfilename = subentry.FileName
Dim t1 As New Threading.Tasks.Task(AddressOf newtransform.doTransform)
t1.Start()
t1.Wait()
GC.Collect()
'sleep here because the build engine takes a while.
Threading.Thread.Sleep(2000)
GC.Collect()
File.Delete(tempdir & entry.FileName)
File.Move(tempdir & Path.GetDirectoryName(entry.FileName) & "/transformed.config", tempdir & entry.FileName)
'put them back into the zip file
filetoadd.Add(tempdir & entry.FileName, Path.GetDirectoryName(entry.FileName))
filestoremove.Add(entry)
Next
End If
Next
Next
'loop through, remove all the "extra configs"
For Each entry As ZipEntry In From a In zip.Entries Where a.IsDirectory = False
Dim removed As Boolean = False
For Each myMatch As Match In SubConfigRegex.Matches(entry.FileName)
If myMatch.Success Then
filestoremove.Add(entry)
removed = True
End If
Next
If removed = False Then
For Each myMatch As Match In SubConfigRegex2.Matches(entry.FileName)
If myMatch.Success Then
filestoremove.Add(entry)
End If
Next
End If
Next
'delete them
For Each File In filestoremove
zip.RemoveEntry(File)
Next
For Each f In filetoadd
zip.AddFile(f.Key, f.Value)
Next
zip.Save()
End Using
Lastly but the most important is where we actually do the replacement of the web.configs.
Public Class doTransform
Property tempdir As String
Property filename As String
Property subfilename As String
Public Function doTransform()
'do the config swap using msbuild
Dim be As New Engine
Dim BuildProject As New BuildEngine.Project(be)
BuildProject.AddNewUsingTaskFromAssemblyFile("TransformXml", "$(MSBuildExtensionsPath)\Microsoft\VisualStudio\v10.0\Web\Microsoft.Web.Publishing.Tasks.dll")
BuildProject.Targets.AddNewTarget("null")
BuildProject.AddNewPropertyGroup(True)
DirectCast(BuildProject.PropertyGroups(0), Microsoft.Build.BuildEngine.BuildPropertyGroup).AddNewProperty("GenerateResourceNeverLockTypeAssemblies", "true")
Dim bt As BuildTask
bt = BuildProject.Targets("null").AddNewTask("TransformXml")
bt.SetParameterValue("Source", tempdir & filename)
bt.SetParameterValue("Transform", tempdir & subfilename)
bt.SetParameterValue("Destination", tempdir & Path.GetDirectoryName(filename) & "/transformed.config")
'bt.Execute()
BuildProject.Build()
be.Shutdown()
End Function
End Class
Like I said... it's difficult but it can be done.

Just to add to this awnser, in order to modify other files than the web.config in an application published with msdeploy (webdeploy) you can set the scope attribute in the parameters.xml file in the root of the project:
<parameters>
<parameter name="MyAppSetting" defaultvalue="_defaultValue_">
<parameterentry match="/configuration/appSettings/add[#key='MyAppSetting']/#value" scope=".exe.config$" kind="XmlFile">
</parameterentry>
</parameter>
</parameters>
scope is a regex that will be used to find files to apply the match xpath to. I havent experimented with this extensively but as far as I understand it simply replaces what ever the xpath matches with the value that is provided later.
There are also other values that can be used for kind that will have different behaviors than an xpath, see https://technet.microsoft.com/en-us/library/dd569084(v=ws.10).aspx for details
note: this applies to when you're using a parameters.xml, not when using the web.config.Debug/Release files

Related

FileSystemObject.FolderExists returning wrong value

The following code:
Set fso = Server.CreateObject("Scripting.FileSystemObject")
If Not fso.FolderExists(path) Then
fso.CreateFolder(path)
End If
Is producing the following error:
Microsoft VBScript runtime error '800a003a' File already exists
If I delete the folder, so that ASP is able to create it itself, it works as expected.
It's only when I manually (or using DOS MKDIR) create it that FolderExists returns false and CreateFolder throws the above error.
What's going on here?
EDIT:
The variable path contains the string C:\Windows\Temp\email_attachments\ and FolderExists seems to be returning false for directories that have been around since before the last startup.
Hi my worked code.
file_path="C:\inetpub\wwwroot\upload\2018\4"
set fso=Server.CreateObject("Scripting.FileSystemObject")
tmpArr = Split(file_path, "\")
tmpPath = tmpArr(0)
For i = 1 To UBound(tmpArr)
If Not fso.FolderExists(Server.MapPath(tmpPath)) Then
fso.CreateFolder Server.MapPath(tmpPath)
End If
tmpPath = tmpPath & "\" & tmpArr(i)
Next

ASP.Net CType unable to convert "£1,234.56" to Decimal - Need detailed list of influences

This is a common question - and it has several common answers.
My problem is that none of them have worked.
The code is entirely typical, a CType to a Decimal, which fails due to globalization settings.
Console.write( CType("£1,234.56", Decimal) )
>>> Conversion from string "£1,234.56" to type 'Decimal' is not valid.
My issue is this particular computer isn't converting the value after several different attempts to fix it.
So far, in no particular order of desperation:
Region settings. Made sure they were (United Kingdom) [en-GB], restarted.
IIS, ".NET Globalisation", Culture is set to Invariant Language (Invariant Country).
IIS, ".NET Globalisation", Culture is set to (United Kingdom) [en-GB].
Region settings, Administrative tab, Copy locale settings.
I've yet to try:
Set in web.config ... https://msdn.microsoft.com/en-us/library/bz9tc508.aspx
Is there anything else I can try?
As the_lotus mentioned, Decimal.Parse will work:
Option Infer On
Imports System.Globalization
Module Module1
Sub Main()
Dim s = "£1,234.56"
Dim d = Decimal.Parse(s, NumberStyles.Currency, New CultureInfo("en-GB"))
Console.WriteLine(d) ' outputs 1234.56
s = "$1,234.56"
d = Decimal.Parse(s, NumberStyles.Currency, New CultureInfo("en-US"))
Console.WriteLine(d) ' outputs 1234.56
If Decimal.TryParse(s, NumberStyles.Currency, New CultureInfo("en-GB"), d) Then
Console.WriteLine(d)
Else
Console.WriteLine("Could not parse.")
End If
Console.ReadLine()
End Sub
End Module
An inferior way would be to remove everything which doesn't belong in a number:
Dim s = "£1,234.56"
Dim re As New Regex("[^0-9,.]")
Dim t = re.Replace(s, "")
Console.WriteLine(CType(t, Decimal)) ' outputs 1234.56
It isn't as good because you don't get to check if the currency symbol was the one you expected.

Having classic ASP read in a key from appsettings in a web.config file

OK so here's the situation. I've got a classic ASP website running inside an MVC 4 application. I need the classic ASP website to be able to get a key from the appsettings section of the web.config file.
Here is the function I've got:
' Imports a site string from an xml file (usually web.config)
Function ImportMySite(webConfig, attrName, reformatMSN)
Dim oXML, oNode, oChild, oAttr, dsn
Set oXML=Server.CreateObject("Microsoft.XMLDOM")
oXML.Async = "false"
oXML.Load(Server.MapPath(webConfig))
Set oNode = oXML.GetElementsByTagName("appSettings").Item(0)
Set oChild = oNode.GetElementsByTagName("add")
' Get the first match
For Each oAttr in oChild
If oAttr.getAttribute("key") = attrName then
dsn = oAttr.getAttribute("mysite")
ImportMySite = dsn
Exit Function
End If
Next
End Function
Here is the function call code:
msn = ImportMySite("web.config", "mysite", false)
So when I call this function the value I get back is always blank or null. I'm not sure where I'm going wrong, I'm a total novice with XML so maybe I'm missing something completely obvious. I have searched the questions but couldn't find anything related to this using classic ASP.
Any help would be much appreciated.
I appreciate Connor's work. It got me well along the way to making this happen. I made some changes that I think might be helpful to others.
I did not want to have to repeat the file name for each call and I have a couple of sections in my config. This seemed more general. Also, I consolidated his changes into a for-sure working example. You can paste this into your app, change the CONFIG_FILE_PATH and get on with your life.
'******************************GetConfigValue*******************************
' Purpose: Utility function to get value from a configuration file.
' Conditions: CONFIG_FILE_PATH must be refer to a valid XML file
' Input: sectionName - a section in the file, eg, appSettings
' attrName - refers to the "key" attribute of an entry
' Output: A string containing the value of the appropriate entry
'***************************************************************************
CONFIG_FILE_PATH="Web.config" 'if no qualifier, refers to this directory. can point elsewhere.
Function GetConfigValue(sectionName, attrName)
Dim oXML, oNode, oChild, oAttr, dsn
Set oXML=Server.CreateObject("Microsoft.XMLDOM")
oXML.Async = "false"
oXML.Load(Server.MapPath(CONFIG_FILE_PATH))
Set oNode = oXML.GetElementsByTagName(sectionName).Item(0)
Set oChild = oNode.GetElementsByTagName("add")
' Get the first match
For Each oAttr in oChild
If oAttr.getAttribute("key") = attrName then
dsn = oAttr.getAttribute("value")
GetConfigValue = dsn
Exit Function
End If
Next
End Function
settingValue = GetConfigValue("appSettings", "someKeyName")
Response.Write(settingValue)
OK I found my answer.
I changed:
For Each oAttr in oChild
If oAttr.getAttribute("key") = attrName then
dsn = oAttr.getAttribute("mysite")
ImportMySite = dsn
Exit Function
End If
Next
To:
For Each oAttr in oChild
If oAttr.getAttribute("key") = attrName then
dsn = oAttr.getAttribute("value")
ImportMySite = dsn
Exit Function
End If
Next

VBS Remove Readonly attribute recursivelty

I'm very new to VBS and experimenting with removing the read only attribute from a directory recursively.
It's removing the read only attribute for files but not for the directories. Also The files within these directories seem to have lost their associated program link and are now all showing as an unregistered file type. Any help is greatly appreciated.
Update: I can see why the files have lost their associations now. It's because the . which separates the name from the extension has been removed! Doh! Ideally I'd like to rename the filename only.
re.Pattern = "[_.]"
re.IgnoreCase = True
re.Global = True
RemoveReadonlyRecursive("T:\Torrents\")
Sub RemoveReadonlyRecursive(DirPath)
ReadOnly = 1
Set oFld = FSO.GetFolder(DirPath)
For Each oFile in oFld.Files
If oFile.Attributes AND ReadOnly Then
oFile.Attributes = oFile.Attributes XOR ReadOnly
End If
If re.Test(oFile.Name) Then
oFile.Name = re.Replace(oFile.Name, " ")
End If
Next
For Each oSubFld in oFld.SubFolders
If oSubFld.Attributes AND ReadOnly Then
oSubFld.Attributes = oSubFld.Attributes XOR ReadOnly
End If
If re.Test(oSubFld.Name) Then
oSubFld.Name = re.Replace(oSubFld.Name, " ")
End If
RemoveReadonlyRecursive(oSubFld.Path)
Next
End Sub
It seems you want to automate a repeatable action by a script. Why don't you use the attrib command to do that for you:
attrib -r "T:\Torrents\*.*" /S
You can place that in a batch file if you want to attach it to an clickable icon.
EDIT:
To run it from VBScript silently:
Set WshShell = WScript.CreateObject("WScript.Shell")
WshShell.Run "attrib -r ""T:\Torrents\*.*"" /S", 0, true)
EDIT2:
To replace everything except the last period, use a regular expression like:
filename = "my file.name.001.2012.extension"
Set regEx = New RegExp
' Make two captures:
' 1. Everything except the last dot
' 2. The last dot and after that everything that is not a dot
regEx.Pattern = "^(.*)(\.[^.]+)$" ' Make two captures:
' Replace everything that is a dot in the first capture with nothing and append the second capture
For each match in regEx.Execute(filename)
newFileName = replace(match.submatches(0), ".", "") & match.submatches(1)
Next

Where do DAC objects live (in Classic ASP)?

I have taken over a departing programmer's Classic ASP object, and I'm trying to debug some DAC code. The trouble is I don't really understand what DAC is or how it works -- and I can't find were the DAC code "lives".
Here's the individual line of code:
set runObj = server.CreateObject("DAC.clsDb_container")
We use SQL Server, and I looked in Enterprise Manager under Stored Procedures and User-Defined functions, but I don't see anything named clsDB_container.
Any suggestions where I can find the code for this DAC object?
The full code in context:
FUNCTION getNewGUID
Dim runCON, runObj, runCMD
DebugWrite( "<BEGIN> iRoutines.asp|getNewGUID (a) GUID=" & GUID & " dealernum=" & dealernum )
set runObj = server.CreateObject("DAC.clsDb_container")
if not runObj.run_query("EXEC sproc_createGUID") then
traperror(runObj.DB_ErrStr)
else
GUID = replace(runObj.get_by_ordinal(0),"-","")
dealernum_len = trim(cstr(len(dealernum)))
set runObj = nothing
end if
getNewGUID = dealernum_len & dealernum & GUID
DebugWrite( "<END> iRoutines.asp|getNewGUID (b) getNewGUID=" & getNewGUID & " dealernum=" & dealernum )
END FUNCTION
This looks like a custom COM object that was written as a data access layer for the site.
The name of the object would be DAC.clsDb_container and lives in a DLL somewhere on the web server.
It is not standard - you will need to look for (I am guessing here) the VB6 or Delphi code that created it if you want to be enlightened further.
if all you need is a GUID then you could do this
<%
Function createGuid()
Set TypeLib = Server.CreateObject("Scriptlet.TypeLib")
dim tg : tg = TypeLib.Guid
createGuid = left(tg, len(tg)-2)
Set TypeLib = Nothing
End Function
%>

Resources