A while back I created a script that removes not registered resources from the contentXXL resource folders, because contentXXL doesn't allow uploading those files through the resource manager once they are already found in the file system.
But since this wasn't an ideal solution and the topic has been bugging me for quite a while, I sat down and started working on a helper app, that automatically registers any new files or folders that don't exist in the contentXXL database.
First, I'll document what I did to setup my helper app
-
I've created a new ASP.NET Web Application.
We prefer to use Web Applications as a project template compared to use a Website project where the code goes into the App_Code folder.
Using the Website template it's of course easy to make quick changes to the code files, as compilation isn't required. However it might get difficult to different projects cleanly organized in your App_Code folder. Also, you're unable to get a nicely name dll file for your addon. With the web application project template, you must compile all changes before you deploy them onto the web server. And therefore will notice any compiler error before deployment.
-
Next, I changed the default project structure like shown in the image below:
It's suggested that you add your contentXXL extensions into the "Addons" folder, within a folder for your company.
-
I'm going to use contentXXL API functions in my project and so I've added a reference to the contentXXL dll
Creating a solution folder in your Visual Studio solution is the best place, especially if more than developers work on the project. Place a copy of the contentXXL dll there and use it to create the reference. This way, you won't deploy the library folder later on, and if a colleague opens the project, he or she will have the referenced dll included in the project.
-
Since I like to use the "Publish" function of Visual Studio (details on this a bit further down), it's important to make a few settings in order to prevent overwriting of important files:
a) web.config - set "Build action" to None -- otherwise you would overwrite contentXXL's own web.config where database configuration and so one is done.
b) contentXXL reference - set "Copy Local" to False and "Specific Version" to False -- you don't want to overwrite the dll file in a installed contentXXL system, which might be different to the version used for the reference
-
The easiest way to deploy a contentXXL extension is done by using the Publish function of Visual Studio. You can find it under the "Build" menu.
If you have a version of contentXXL installed on your local machine (which is the solution if you're developing things and would like to debug), you can deploy the solution to the root folder of you contentXXL website.
Choose "File System" as the publish method and select the root folder of contentXXL as target location.
If I need to deploy somewhere else, I usually deploy into a folder on my notebook and simply copy the contents to the desired location. Not as convenient as direct publishing though.
-
If you want to debug the solution, it's usually not as easy as simply hitting F5, since the contentXXL modules or addons often just contain usercontrols, that can't be viewed directly. Or, if you're working with the contentXXL API you need the contentXXL environment locals, which you'll only have if you're running in the same application context as contentXXL (id of portal, current language etc.).
The only way to debug projects for contentXXL in most cases is to attach the debugger to the worker process, which in my case is w3wp.exe (as shown in the image below).
On a Windows XP system it would be the aspnet_wp.exe.
Now since everything’s set up, let's move on to the implementation:
-
The goal is to register any files or folders within a portal’s resource folder that are not yet registered in contentXXL. By the way, this is not going to be a full-blown solution with all bells and whistles at this moment, but rather a first “developer’s” version to get the job done. So, I’ll simply put all logic within a webpage’s Page_Load event.
First of all, a method that recursively inspects the file system is required:
|
1
2
3
4
5
|
protected void Page_Load(object sender, EventArgs e)
{
string basePath = Portal.ResourseDir;
CheckFolder(basePath);
}
|
ContentXXL.Runtime.Utility.Portal.ResourseDir returns the physical path of a portal’s resource folder. The CheckFolder method will recursively check each file and folder against the contents of the contentXXL tables Folder and Resource.
-
Since there are now suitable API methods to get a simple list of the files or folders within a given path, I’ll use a Linq to SQL context to query the database directly.
Be careful: Adding a “Linq to SQL Classes” file to your project will add new web.config file into the project root folder. So it’s good to remember to either move that file to a subfolder or set “Build Action” to None to prevent it from being deployed.
-
Next I created a data connection to a local contentXXL database in the Server Explorer and dragged the Folder and Resource table to the datacontext.
-
As a last step, I fixed the namespace properties of the datacontext to my solution namespace.
-
Next I’ll implement the method CheckFolder.
The basic idea is to get a list of files and subfolders in contentXXL for a given path and to compare the results to the Directory.GetFiles() or Directory.GetDirectories() methods for same path.
Since the CheckResource folder is being called with a physical path, but contentXXL is using relative paths in the database, the relative path must be extracted.
|
1
2
3
4
5
6
7
8
|
void CheckFolder(string physicalPath)
{
List<string></string> resources;
List<string></string> resourceFolders;
try
{
string relPath = "/" + physicalPath.Replace(Portal.ResourseDir, string.Empty).Replace(@"\", "/");
|
Then, the Linq to SQL context is queried to get the files and folders.
|
1
2
3
4
5
6
7
8
9
10
11
|
using (var dc = new ContentXXLDataContext(Params.ConnectionString))
{
resources = (from x in dc.Resources
where x.Path == relPath && x.PortalID == Portal.PortalID
select x.FileName).ToList();
resourceFolders = (from x in dc.Folders
where x.Path == relPath && x.PortalID == Portal.PortalID
select x.Name).ToList();
}
|
The next step is to get the files for the current folder and to check, if the file already exists in contentXXL.
|
1
2
3
4
5
6
7
8
9
10
11
|
string[] files = Directory.GetFiles(physicalPath);
foreach (var file in files)
{
string fileName = Path.GetFileName(file);
if (!resources.Contains(fileName))
{
RegisterNewResource(file, fileName, relPath);
}
}
|
And finally, check for the subfolders of the given path if they exist and handle any existing subfolder contents.
|
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
|
string[] folders = Directory.GetDirectories(physicalPath);
foreach (var folder in folders)
{
string folderName = folder.Replace(Path.GetDirectoryName(folder), string.Empty).Substring(1);
if (!resourceFolders.Contains(folderName))
{
string validatedFolderName;
if (RegisterNewFolder(folder, folderName, relPath, out validatedFolderName))
{
CheckFolder(Path.GetDirectoryName(folder) + @"\" + validatedFolderName);
}
}
else
{
CheckFolder(folder);
}
}
}
catch (Exception ex)
{
throw;
}
}
|
-
The method RegisterNewResource takes care of the not yet registered files.
I'm going to ignore web.config or Thumbs.db files, as they are created by IIS or Windows itself.
|
1
2
3
4
|
private void RegisterNewResource(string file, string fileName, string relPath)
{
if (fileName == "web.config" || fileName == "Thumbs.db")
return;
|
contentXXL requires a filename without spaces etc., therefore I need to run the filename through the function ValidateFileName. However, it be the case that a validated filename already exists in contentXXL so I decided, in this version I'm going to ignore these files and just add a note to the output. Of course the files also needs to be renamed to the new name.
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
string validatedName = FileNameValidator.ValidateFileName(fileName);
if (validatedName != fileName)
{
if (!File.Exists(Path.GetDirectoryName(file) + @"\" + validatedName))
{
File.Move(file, Path.GetDirectoryName(file) + @"\" + validatedName);
Response.Write(String.Format("Renamed file {1}{0} to: {1}{2}<br>",
fileName, relPath, validatedName));
}
else
{
Response.Write(String.Format("Error: couldn't rename file {1}/{0} to: {1}{2} as a file with that name already existed.<br>",
fileName, relPath, validatedName));
return;
}
}
|
When nothing stands in the way, all that needs to be done is the actual registering of the resource, which can be done through the contentXXL API. The new resource needs to be created and saved. In order for it to be visible in contentXXL it also requires certain permission, so the permissions of the parent folder will be applied.
|
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
|
var resource = new Resource()
{
PortalID = Portal.PortalID,
Title = fileName,
Description = string.Empty,
FileName = validatedName,
Path = relPath
};
resource.Save();
var parentFolder = new ResourceFolder(Portal.PortalID);
parentFolder.Load(relPath);
if (!string.IsNullOrEmpty(resource.GlobalID))
{
GrantAccess.CopyTo(parentFolder.GlobalID, resource.GlobalID);
}
else
{
Policy.AssignDefaultPolicyTo(parentFolder.GlobalID, resource.PortalID);
}
Response.Write(string.Format("Registered new resource: {0}{1}<br>", relPath, fileName));
}
|
-
The method RegisterNewFolder takes care of the folders and works quite similar to the file method.
It's also required to validate the foldername before it can be created as a contentXXL resource folder. If a renamed folder already exists, again it will be ignored in this version. If it doesn't exists, the folder is renamed to the new, validated folder name.
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
private bool RegisterNewFolder(string folder, string folderName, string relPath, out string validatedName)
{
try
{
validatedName = FileNameValidator.ValidatePath(folderName);
if (validatedName != folderName)
{
var newPath = Path.GetDirectoryName(folder) + @"\" + validatedName;
if (!Directory.Exists(newPath))
{
Directory.Move(folder, newPath);
Response.Write(String.Format("Renamed folder {1}{0} to: {1}{2}<br>", folderName, relPath, validatedName));
}
else
{
Response.Write(String.Format("Error: couldn't rename folder {1}{0} to: {1}{2} as a folder with that name already existed.<br>",
folderName, relPath, validatedName));
return false;
}
}
|
Then the contentXXL API is also used to create the new folder with the required permissions. And finally, the contentXXL cache object that stores the folder structures is reset.
|
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
|
var parentFolder = new ResourceFolder(Portal.PortalID);
parentFolder.Load(relPath);
var cxxlFolder = new ResourceFolder(Portal.PortalID)
{
Name = validatedName,
Path = relPath,
ParentID = parentFolder.FolderID
};
cxxlFolder.Save();
if (!string.IsNullOrEmpty(cxxlFolder.GlobalID))
{
GrantAccess.CopyTo(parentFolder.GlobalID, cxxlFolder.GlobalID);
}
else
{
Policy.AssignDefaultPolicyTo(parentFolder.GlobalID, cxxlFolder.PortalID);
}
DP.GetCustom("xxlfolders").Reset();
Response.Write(string.Format("Registered new folder: {0}{1}<br>",
relPath, validatedName));
}
catch (Exception ex)
{
throw;
}
return true;
}
|
Hope this helps, and if you've got any questions or comments, post them in the comments sections below.
When downloading either the source or the executable please remeber, this is just a prototype - opening the file in the browser will run the code, and the code so far doesn't include any checks for authorization and it would be accessible for anyone knowing the URL.
As always: use at your own risk!