I'm trying to use an SSIS script task to copy an excel file into a Sharepoint library when a file with the same name already exists there. It doesn't matter if it deletes the old file first then copies the new or just copies and replaces the new. I can't figure out how to delete the old file and it won't copy the new until the old is gone. So far I have:
/*
Microsoft SQL Server Integration Services Script Task
Write scripts using Microsoft Visual C# 2008.
The ScriptMain is the entry point class of the script.
*/
using System;
using System.Data;
using Microsoft.SqlServer.Dts.Runtime;
using System.Windows.Forms;
using System.IO;
namespace ST_be5f0817a6b54483a96a8c9e79402175.csproj
{
[System.AddIn.AddIn("ScriptMain", Version = "1.0", Publisher = "", Description = "")]
public partial class ScriptMain : Microsoft.SqlServer.Dts.Tasks.ScriptTask.VSTARTScriptObjectModelBase
{
#region VSTA generated code
enum ScriptResults
{
Success = Microsoft.SqlServer.Dts.Runtime.DTSExecResult.Success,
Failure = Microsoft.SqlServer.Dts.Runtime.DTSExecResult.Failure
};
#endregion
/*
The execution engine calls this method when the task executes.
To access the object model, use the Dts property. Connections, variables, events,
and logging features are available as members of the Dts property as shown in the following examples.
To reference a variable, call Dts.Variables["MyCaseSensitiveVariableName"].Value;
To post a log entry, call Dts.Log("This is my log text", 999, null);
To fire an event, call Dts.Events.FireInformation(99, "test", "hit the help message", "", 0, true);
To use the connections collection use something like the following:
ConnectionManager cm = Dts.Connections.Add("OLEDB");
cm.ConnectionString = "Data Source=localhost;Initial Catalog=AdventureWorks;Provider=SQLNCLI10;Integrated Security=SSPI;Auto Translate=False;";
Before returning from this method, set the value of Dts.TaskResult to indicate success or failure.
To open Help, press F1.
*/
public void Main()
{
string fileDir = (string)Dts.Variables["fileDir"].Value;
string SPDir = (string)Dts.Variables["SPDir"].Value;
if (File.Exists(Path.Combine(SPDir, "filename.CSV")))
{
File.Delete(Path.Combine(SPDir, "filename.CSV"));
}
File.Copy(Path.Combine(fileDir, "filename.CSV"), Path.Combine(SPDir, "filename.CSV"));
Dts.TaskResult = (int)ScriptResults.Success;
}
}
}
I think I can delete the file by running a data flow component with the sharepoint list connector before the script and have the script just copy the file over, but I'm trying to avoid that many components and connections and that method just generally sounds more complicated.
Any help or advice would be welcome.
Fixed this issue by changing the relevant line to: File.Copy(Path.Combine(fileDir, "filename.CSV"), Path.Combine(SPDir, "filename.CSV"), true);
The boolean argument specifies whether to overwrite the existing file and is "false" if not specified.
Related
My team is working on an Office 365 add-in for Excel, and as part of the project, we’re creating Excel documents through the GraphAPI with the end goal of having the add-in already setup for the document. We’re using the .NET OpenXml library to create the document before copying it through the GraphAPI.
We haven’t been able to find many resources for how to setup an add-in through OpenXml and have not been able to get anything working. The last thing we tried was copying the example we found here, but we couldn’t get it working. Does anyone know how to setup add-ins using the OpenXml library?
Note: the add-in is already in the Office Add-Ins store, and we have information like the AppSource ID.
Thank you!
We're actually about to publish a new sample around this scenario. The sample shows how to create an Excel document using OOXML, embed your add-in, and then upload the file to OneDrive. It also creates a Team chat that links to the file.
You can try out the sample here: Open data from your web site in a spreadsheet in Microsoft Teams
Or give us feedback on the PR: https://github.com/OfficeDev/PnP-OfficeAddins/pull/197
To answer your question about how to embed the add-in, you need to create a web extension section. I've copied the relevant code here. Note this is the same code from the Office-OOXML-EmbedAddin sample you already looked at. We reused it for the new sample. You can change the CUSTOM MODIFICATION section to provide any custom properties you want to your add-in when it opens.
// Embeds the add-in into a file of the specified type.
private void EmbedAddin(SpreadsheetDocument spreadsheet)
{
spreadsheet.DeletePart(spreadsheet.WebExTaskpanesPart);
var webExTaskpanesPart = spreadsheet.AddWebExTaskpanesPart();
CreateWebExTaskpanesPart(webExTaskpanesPart);
}
// Adds child parts and generates content of the specified part.
private void CreateWebExTaskpanesPart(WebExTaskpanesPart part)
{
WebExtensionPart webExtensionPart1 = part.AddNewPart<WebExtensionPart>("rId1");
GenerateWebExtensionPart1Content(webExtensionPart1);
GeneratePartContent(part);
}
// Generates content of webExtensionPart1.
private void GenerateWebExtensionPart1Content(WebExtensionPart webExtensionPart1)
{
// Add web extension containg Id for Script Lab add-in
We.WebExtension webExtension1 = new We.WebExtension() { Id = "{635BF0CD-42CC-4174-B8D2-6D375C9A759E}" };
webExtension1.AddNamespaceDeclaration("we", "http://schemas.microsoft.com/office/webextensions/webextension/2010/11");
// Add store information for Script Lab add-in
We.WebExtensionStoreReference webExtensionStoreReference1 = new We.WebExtensionStoreReference() { Id = "wa104380862", Version = "1.1.0.0", Store = "en-US", StoreType = "OMEX" };
We.WebExtensionReferenceList webExtensionReferenceList1 = new We.WebExtensionReferenceList();
We.WebExtensionPropertyBag webExtensionPropertyBag1 = new We.WebExtensionPropertyBag();
// Add the property that makes the taskpane visible.
We.WebExtensionProperty webExtensionProperty1 = new We.WebExtensionProperty() { Name = "Office.AutoShowTaskpaneWithDocument", Value = "true" };
webExtensionPropertyBag1.Append(webExtensionProperty1);
// CUSTOM MODIFICATION BEGIN
// Add the property that specifies the snippet to import.
string snippetToImportValue = string.Format("{{\"type\":\"gist\",\"id\":\"{0}\"}}", "{72189570-AE11-4207-9DEE-C8BDE4B83188}");
We.WebExtensionProperty webExtensionProperty2 = new We.WebExtensionProperty() { Name = "SnippetToImport", Value = snippetToImportValue };
webExtensionPropertyBag1.Append(webExtensionProperty2);
// CUSTOM MODIFICATION END
We.WebExtensionBindingList webExtensionBindingList1 = new We.WebExtensionBindingList();
We.Snapshot snapshot1 = new We.Snapshot();
snapshot1.AddNamespaceDeclaration("r", "http://schemas.openxmlformats.org/officeDocument/2006/relationships");
webExtension1.Append(webExtensionStoreReference1);
webExtension1.Append(webExtensionReferenceList1);
webExtension1.Append(webExtensionPropertyBag1);
webExtension1.Append(webExtensionBindingList1);
webExtension1.Append(snapshot1);
webExtensionPart1.WebExtension = webExtension1;
}
// Generates content of part.
private void GeneratePartContent(WebExTaskpanesPart part)
{
Wetp.Taskpanes taskpanes1 = new Wetp.Taskpanes();
taskpanes1.AddNamespaceDeclaration("wetp", "http://schemas.microsoft.com/office/webextensions/taskpanes/2010/11");
Wetp.WebExtensionTaskpane webExtensionTaskpane1 = new Wetp.WebExtensionTaskpane() { DockState = "right", Visibility = true, Width = 350D, Row = (UInt32Value)4U };
Wetp.WebExtensionPartReference webExtensionPartReference1 = new Wetp.WebExtensionPartReference() { Id = "rId1" };
webExtensionPartReference1.AddNamespaceDeclaration("r", "http://schemas.openxmlformats.org/officeDocument/2006/relationships");
webExtensionTaskpane1.Append(webExtensionPartReference1);
taskpanes1.Append(webExtensionTaskpane1);
part.Taskpanes = taskpanes1;
}
I have a custom module getting executed right after the PDFGenerator finished. I followed this guide on how to create a custom module
https://stackoverflow.com/a/55799101/9945420
When processing a batch document I want to manipulate the generated PDF file and add a footer to that file. The content of that footer needs to get configured in the Administration module.
So within my project called "StampOnScanProcess" I added a Folder called "Setup" with two files. A Form called "FrmSetup"
public partial class FrmSetup : Form
{
private IBatchClass batchClass;
public FrmSetup()
{
InitializeComponent();
}
public DialogResult ShowDialog(IBatchClass batchClass)
{
this.batchClass = batchClass;
// Load previous Settings ...
return this.ShowDialog();
}
private void btnCancel_Click(object sender, EventArgs e)
{
this.Close();
}
private void btnSave_Click(object sender, EventArgs e)
{
// Save ...
this.Close();
}
}
and a UserControl called "UserCtrlSetup"
[InterfaceType(ComInterfaceType.InterfaceIsIDispatch)]
public interface ISetupForm
{
[DispId(1)]
AdminApplication Application { set; }
[DispId(2)]
void ActionEvent(int EventNumber, object Argument, out int Cancel);
}
[ClassInterface(ClassInterfaceType.None)]
[ProgId(CUSTOM_MODULE_NAME_SETUP)]
public partial class UserCtrlSetup : UserControl, ISetupForm
{
private const string CUSTOM_MODULE_NAME_SETUP = "StampOnScanProcess.Setup";
private AdminApplication adminApplication;
public AdminApplication Application
{
set
{
value.AddMenu(CUSTOM_MODULE_NAME_SETUP, CUSTOM_MODULE_NAME_SETUP, "BatchClass");
adminApplication = value;
}
}
public void ActionEvent(int EventNumber, object Argument, out int Cancel)
{
Cancel = 0;
if ((KfxOcxEvent)EventNumber == KfxOcxEvent.KfxOcxEventMenuClicked && (string)Argument == CUSTOM_MODULE_NAME_SETUP)
{
FrmSetup form = new FrmSetup();
form.ShowDialog(adminApplication.ActiveBatchClass);
}
}
}
I modified my registration file and added the setup form to it
[Modules]
StampOnScanProcess
[StampOnScanProcess]
RuntimeProgram=StampOnScanProcess.exe
ModuleID=StampOnScanProcess.exe
Description=...
Version=10.2
SupportsNonImageFiles=True
SupportsTableFields=True
SetupProgram=StampOnScanProcess.Setup
[Setup Programs]
StampOnScanProcess.Setup
[StampOnScanProcess.Setup]
Visible=0
OCXFile=StampOnScanProcess.exe
ProgID=StampOnScanProcess.Setup
When launching the Administration module I head over to the Batch Class Properties => Queues and want to call this setup form by clicking the Properties button in the middle.
Unfortunately the properties button is disabled so I can't open the setup form. This form gets added to the context menu of the batch class
How can I bind this form to the properties button instead? And what is the best way to store configured data and access it when the runtime application gets executed?
I need to think about how to store data because some users have user profiles
and the runtime application currently logs in with no credentials.
public void LoginToRuntimeSession()
{
login = new Login();
login.EnableSecurityBoost = true;
login.Login();
login.ApplicationName = CUSTOM_MODULE_ID;
login.Version = "1.0";
login.ValidateUser($"{CUSTOM_MODULE_ID}.exe", false, "", "");
session = login.RuntimeSession;
}
So it might happen that I have to store the credentials on setup too.
How can I bind this form to the properties button instead?
All interactions with menu entries are handled by ISetupForm.ActionEvent. New entries are added with the AddMenu method of the AdminApplication object. Kofax differentiates between multiple entries by name - imagine that you could have multiple menu entries at the same time, one on batch class level, another one on document class level, and another one in the ribbon - just to name a few examples. Kofax uses the same approach in any component that integrates into Administration (e.g. Custom Modules or Workflow Agents).
This is an example from one of our components. Note that three entries are added on BatchClass level and two more on DocumentClass level.
value.AddMenu("BatchClass.GeneralConfig", "Field Panel - General Configuration", "BatchClass");
value.AddMenu("BatchClass.FieldEditor", "Field Panel - Configure Batch Fields", "BatchClass");
value.AddMenu("DocumentClass.FieldEditor", "Field Panel - Configure Index Fields", "DocumentClass");
value.AddMenu("CopyBatchFieldConfig", "Field Panel - Copy Batch Field Configuration", "BatchClass");
value.AddMenu("PasteBatchFieldConfig", "Field Panel - Paste Batch Field Configuration", "BatchClass");
value.AddMenu("CopyIndexFieldConfig", "Field Panel - Copy Index Field Configuration", "DocumentClass");
value.AddMenu("PasteIndexFieldConfig", "Field Panel - Paste Index Field Configuration", "DocumentClass");
Each entry is no identified by its event text, the first parameter. For example, BatchClass.GeneralConfig is intended to open up a generic configuration dialog - on batch class level.
Now, back to our ActionEvent - this is how I distinguish between the entry selected by the user:
if ((KfxOcxEvent)EventNumber == KfxOcxEvent.KfxOcxEventMenuClicked)
{
AdminForm form = new AdminForm();
switch ((string)Argument)
{
case "BatchClass.GeneralConfig":
ConfigureGeneral(kcApp.ActiveBatchClass);
break;
[I] want to call this setup form by clicking the Properties button in
the middle.
I don't know if you can use this button - I would assume yes - yet personally I tend to put settings either on batch or document class level. For example - your PDF annotation settings may different from document class to class - having an entry on this level seems more natural.
And what is the best way to store configured data and access it when
the runtime application gets executed?
Custom Storage Strings, and you can let your imagination run wild here. The most simplistic approach is to store key-value pairs during setup, and retrieve them in runtime. Here's a generic call (BatchClass is an IBatchClass object, i.e. a pointer to the ActiveBatchClass property of the AdminApplication object):
// set a CSS
BatchClass.set_CustomStorageString(name, value);
// get a CSS
BatchClass.get_CustomStorageString(name)
I usually use a single custom storage string only and store custom object - the object is a base64-encoded serialized XML using XmlSerializer - but again, that's up to you. The only recommendation is to rely on CSS only - don't use external files to store configuration parameters. A CSS is an integral part of your batch class - so, when exporting said class and importing it on a different system, your entire configuration will be there.
I need to think about how to store data because some users have user
profiles
Usually, you don't need to worry about that. The properties for user and password in ValidateUser are entirely optional - and since you're planning to write an unattended module - ideally a Windows Service, credentials should be maintained there. Kofax and Windows would automatically make sure the credentials are passed on, and your module will run under this user's context. Just make sure the user has permissions for the module and all associated batch classes. It's different if you're planning to write an attended module, for example an enhanced Validation module.
I want to create a custom Kofax module. When it comes to the batch processing the scanned documents get converted to PDF files. I want to fetch these PDF files, manipulate them (add a custom footer to the PDF document) and hand them back to Kofax.
So what I know so far:
create Kofax export scripts
add a custom module to Kofax
I have the APIRef.chm (Kofax.Capture.SDK.CustomModule) and the CMSplit as an example project. Unfortunately I struggle getting into it. Are there any resources out there showing step by step how to get into custom module development?
So I know that the IBatch interface represents one selected batch and the IBatchCollection represents the collection of all batches.
I would just like to know how to setup a "Hello World" example and could add my code to it and I think I don't even need a WinForms application because I only need to manipulate the PDF files and that's it...
Since I realized that your question was rather about how to create a custom module in general, allow me to add another answer. Start with a C# Console Application.
Add Required Assemblies
Below assemblies are required by a custom module. All of them reside in the KC's binaries folder (by default C:\Program Files (x86)\Kofax\CaptureSS\ServLib\Bin on a server).
Setup Part
Add a new User Control and Windows Form for setup. This is purely optional - a CM might not even have a setup form, but I'd recommend adding it regardless. The user control is the most important part, here - it will add the menu entry in KC Administration, and initialize the form itself:
[InterfaceType(ComInterfaceType.InterfaceIsIDispatch)]
public interface ISetupForm
{
[DispId(1)]
AdminApplication Application { set; }
[DispId(2)]
void ActionEvent(int EventNumber, object Argument, out int Cancel);
}
[ClassInterface(ClassInterfaceType.None)]
[ProgId("Quipu.KC.CM.Setup")]
public class SetupUserControl : UserControl, ISetupForm
{
private AdminApplication adminApplication;
public AdminApplication Application
{
set
{
value.AddMenu("Quipu.KC.CM.Setup", "Quipu.KC.CM - Setup", "BatchClass");
adminApplication = value;
}
}
public void ActionEvent(int EventNumber, object Argument, out int Cancel)
{
Cancel = 0;
if ((KfxOcxEvent)EventNumber == KfxOcxEvent.KfxOcxEventMenuClicked && (string)Argument == "Quipu.KC.CM.Setup")
{
SetupForm form = new SetupForm();
form.ShowDialog(adminApplication.ActiveBatchClass);
}
}
}
Runtime Part
Since I started with a console application, I could go ahead and put all the logic into Program.cs. Note that is for demo-purposes only, and I would recommend adding specific classes and forms later on. The example below logs into Kofax Capture, grabs the next available batch, and just outputs its name.
class Program
{
static void Main(string[] args)
{
AppDomain.CurrentDomain.AssemblyResolve += (sender, eventArgs) => KcAssemblyResolver.Resolve(eventArgs);
Run(args);
return;
}
static void Run(string[] args)
{
// start processing here
// todo encapsulate this to a separate class!
// login to KC
var login = new Login();
login.EnableSecurityBoost = true;
login.Login();
login.ApplicationName = "Quipu.KC.CM";
login.Version = "1.0";
login.ValidateUser("Quipu.KC.CM.exe", false, "", "");
var session = login.RuntimeSession;
// todo add timer-based polling here (note: mutex!)
var activeBatch = session.NextBatchGet(login.ProcessID);
Console.WriteLine(activeBatch.Name);
activeBatch.BatchClose(
KfxDbState.KfxDbBatchReady,
KfxDbQueue.KfxDbQueueNext,
0,
"");
session.Dispose();
login.Logout();
}
}
Registering, COM-Visibility, and more
Registering a Custom Module is done via RegAsm.exe and ideally with the help of an AEX file. Here's an example - please refer to the documentation for more details and all available settings.
[Modules]
Minimal CM
[Minimal CM]
RuntimeProgram=Quipu/CM/Quipu.KC.CM/Quipu.KC.CM.exe
ModuleID=Quipu.KC.CM.exe
Description=Minimal Template for a Custom Module in C#
Version=1.0
SupportsTableFields=True
SupportsNonImageFiles=True
SetupProgram=Minimal CM Setup
[Setup Programs]
Minimal CM Setup
[Minimal CM Setup]
Visible=0
OCXFile=Quipu/CM/Quipu.KC.CM/Quipu.KC.CM.exe
ProgID=Quipu.KC.CM.Setup
Last but not least, make sure your assemblies are COM-visible:
I put up the entire code on GitHub, feel free to fork it. Hope it helps.
Kofax exposes a batch as an XML, and DBLite is basically a wrapper for said XML. The structure is explained in AcBatch.htm and AcDocs.htm (to be found under the CaptureSV directory). Here's the basic idea (just documents are shown):
AscentCaptureRuntime
Batch
Documents
Document
A single document has child elements itself such as pages, and multiple properties such as Confidence, FormTypeName, and PDFGenerationFileName. This is what you want. Here's how you would navigate down the document collection, storing the filename in a variable named pdfFileName:
IACDataElement runtime = activeBatch.ExtractRuntimeACDataElement(0);
IACDataElement batch = runtime.FindChildElementByName("Batch");
var documents = batch.FindChildElementByName("Documents").FindChildElementsByName("Document");
for (int i = 0; i < documents.Count; i++)
{
// 1-based index in kofax
var pdfFileName = documents[i + 1]["PDFGenerationFileName"];
}
Personally, I don't like this structure, so I created my own wrapper for their wrapper, but that's up to you.
With regard to the custom module itself, the sample shipped is already a decent start. Basically, you would have a basic form that shows up if the user launches the module manually - which is entirely optional if work happens in the back, preferably as Windows Service. I like to start with a console application, adding forms only when needed. Here, I would launch the form as follows, or start the service. Note that I have different branches in case the user wants to install my Custom Module as service:
else if (Environment.UserInteractive)
{
// run as module
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
Application.Run(new RuntimeForm(args));
}
else
{
// run as service
ServiceBase.Run(new CustomModuleService());
}
}
The runtime for itself just logs you into Kofax Capture, registers event handlers, and processes batch by batch:
// login to KC
cm = new CustomModule();
cm.Login("", "");
// add progress event handlers
cm.BatchOpened += Cm_BatchOpened;
cm.BatchClosed += Cm_BatchClosed;
cm.DocumentOpened += Cm_DocumentOpened;
cm.DocumentClosed += Cm_DocumentClosed;
cm.ErrorOccured += Cm_ErrorOccured;
// process in background thread so that the form does not freeze
worker = new BackgroundWorker();
worker.DoWork += (s, a) => Process();
worker.RunWorkerAsync();
Then, your CM fetches the next batch. This can either make use of Kofax' Batch Notification Service, or be based on a timer. For the former, just handle the BatchAvailable event of the session object:
session.BatchAvailable += Session_BatchAvailable;
For the latter, define a timer - preferrably with a configurable polling interval:
pollTimer.Interval = pollIntervalSeconds * 1000;
pollTimer.Elapsed += PollTimer_Elapsed;
pollTimer.Enabled = true;
When the timer elapses, you could do the following:
private void PollTimer_Elapsed(object sender, System.Timers.ElapsedEventArgs e)
{
mutex.WaitOne();
ProcessBatches();
mutex.ReleaseMutex();
}
I would like to upload a spreadsheet of lot/serial #'s into the Allocation popup on the Purchase Receipts screen. It's not uncommon for my company to receive 1,000+ serial #'s in an order and entering them one-at-a-time via this popup is too cumbersome. (My serial numbers aren't sequential, so I can't use the Generate tool.)
I've found a related post here, but I'm unable make the source work.
How to include a dialog for file upload
... begin snippet ...
byte[] filedata = info.BinData;
using (NVExcelReader reader = new NVExcelReader())
{
Dictionary<UInt32, string[]> data = reader.loadWorksheet(filedata);
foreach (string[] textArray in data.Values)
{
// do stuff
}
}
...
The code references a class called NVExcelReader(). Where does this class originate from? Is this part of stock Acumatica? I've been unable to find this class in the source. I'm using Acumatica 2017 R2. Is it possible this class was renamed or moved in newer versions?
Can someone point me in the right direction or explain how I might go about recreating the functionality of NVExcelReader() in Acumatica?
NVExcelReader is not an Acumatica class, the main idea here is to use any existing class to read the excel file.
So what you really need to do:
declare PXUploadDialog element in your aspx file
<px:PXUploadDialog ID="ImportPanel" runat="server" Key="NewRevisionPanel" Height="120px" Style="position: static" Width="560px"
Caption="Import XML File (*.xml)" AutoSaveFile="false" RenderCheckIn="false" SessionKey="ImportStatementProtoFile" />
add a button delegate
public PXSelect<PO.POReceipt> NewRevisionPanel;
public PXAction<PO.POReceipt> ImportAllocations;
[PXUIField(DisplayName = "Import Allocations",
MapEnableRights = PXCacheRights.Update,
MapViewRights = PXCacheRights.Update,
Enabled = true)]
[PXButton()]
public virtual void importAllocations()
{
}
Get selected file data using PXInfo class
const string PanelSessionKey = "ImportStatementProtoFile";
PX.SM.FileInfo info = PX.Common.PXContext
.SessionTyped<PXSessionStatePXData>()
.FileInfo[PanelSessionKey] as PX.SM.FileInfo;
System.Web.HttpContext.Current.Session.Remove(PanelSessionKey);
if (info != null)
{
// here is your file data in bytes
byte[] filedata = info.BinData;
read your excel file in bytes using any existing library. Note this step is not related to Acumatica. You can find helpful information here and here for example
then set values from the file to Acumatica entity (POReceiptLineSplit for example)
Base.splits.Insert(new PO.POReceiptLineSplit()
{
InventoryID = Base.transactions.Current.InventoryID,
LocationID = Base.transactions.Current.LocationID,
LotSerialNbr = valueFromExcelFile1,
Qty = valueFromExcelFile2
});
NVExcelReader is not part of Acumatica framework. I say this because neither resharper was able to find NVExcelReader in Acumatica dlls nor search of string in Acumatica directory was able to find any file that contained NVExcelReader string value. Also Google search for NVExcelReader class doesn't give any good results beside referencing your thread on stackoverflow. In order to recreate NVExcelReader in Acumatica you can consider usage of some third party library which can read from excel files. There are plenty of options starting from COM interfaces, OLE DB for excel and Aspose library for parsing xml files.
We're running SharePoint 2007 SP1 and profiles are imported from Active Directory (a full import runs daily). We had a problem where many of the users were disabled unintentionally in Active Directory and this caused their profiles to be removed from SharePoint. We re-enabled their Active Directory accounts and ran a full import which restored their SharePoint profiles. However, all of their My Links are missing. Is there a method or best practice for restoring them?
I posted this because I couldn't find an answer to my problem anywhere. This post by Joel Oleson that describes a similar problem to mine gave me a hint as to where to go looking for the missing data. And This post by Corey Roth showed me how to programatically add the links to a users My Links.
First things first - you need to restore a backup of the database that contains the My Links data. You don't want to restore over your working database, you want to restore it to another location. The links stored in the SSP Database. (You can find out the name of the database by going into Central Admin --> Shared Services Admin then open the menu for the SSP and click on Edit Properties - the SSP Database is listed on the properties page.)
Once the database has been restored you want to retrieve the Link information:
the domain account name of the user who owns the link,
the url of the link
the name of the link
This query will get you that information:
SELECT UPF.NTName, UL.Url, UL.Title
FROM UserLinks UL INNER JOIN UserProfile_full UPF ON UL.recordID = UPF.recordID
INNER JOIN UserPrivacyPolicy UPP ON UL.PolicyId = UPP.id
ORDER BY NTName
(I should note that I did not take into account what group or what privacy level the links were set to, you could probably find that information by looking at the information in the UserPrivacyPolicy table)
I copied the results into Excel & saved it as a .csv file (comma separated list) - just because my production server did not have access to the location where I restored my database. I ordered the columns with Title last because the Title could contain commas and that would mess up how I'm reading in the data. (I checked and the other two fields do not contain commas - you should check yours before making this assumption.)
I then wrote a little console app to import the data. It takes two arguments:
the path where the file containing all of the links is located (ie c:\temp\links.csv)
the url of the SSP from with the My Links have gone missing (ie https://portal.mydomain.com)
These are the references used:
Microsoft.Office.Server (C:\Program Files\Common Files\Microsoft Shared\Web Server Extensions\12\ISAPI\Microsoft.Office.Server.dll)
Microsoft.SharePoint (C:\Program Files\Common Files\Microsoft Shared\Web Server Extensions\12\ISAPI\Microsoft.SharePoint.dll)
System
System.Data
System.Web
System.Xml
And this is the code:
using System;
using System.IO;
using System.Collections.Generic;
using System.Text;
using Microsoft.Office.Server;
using Microsoft.Office.Server.UserProfiles;
using Microsoft.SharePoint;
using Microsoft.SharePoint.Administration;
using System.Web;
namespace UserLinks
{
class Program
{
static void Main(string[] args)
{
string _accountName = "", _linkTitle = "", _url = "", _tmp = "", _path = "", _SPSsite = "";
// Check arguments
if (args.Length != 2)
{
ShowUsage();
return;
}
_path = args[0];
_SPSsite = args[1];
using (SPSite _site = new SPSite(_SPSsite))
{
ServerContext _context = ServerContext.GetContext(_site);
UserProfileManager _userProfileManger = new UserProfileManager(_context);
/* Expecting a comma seperated list with 3 columns:
* AccountName in the format Domain\Account name - I am assuming there are no commas in this field
* URL - I am assuming there are no commas in this field
* Link Title - link title is last because there may be commas in the title
*/
TextReader _reader = new StreamReader(_path, System.Text.Encoding.Default);
while (_reader.Peek() != -1)
{
_tmp = _reader.ReadLine();
_accountName = _tmp.Substring(0, _tmp.IndexOf(','));
_tmp = _tmp.Replace(_accountName + ",", "");
_url = _tmp.Substring(0, _tmp.IndexOf(','));
_linkTitle = _tmp.Replace(_url + ",", "");
try
{
UserProfile _currentUser = _userProfileManger.GetUserProfile(_accountName);
QuickLinkManager _quickLinkManager = _currentUser.QuickLinks;
_quickLinkManager.Create(_linkTitle, _url, QuickLinkGroupType.General, null, Privacy.Private); //I am assuming that all links have no group assigned to them and they are all private links
}
catch (Exception ex)
{
Console.WriteLine(_accountName);
Console.WriteLine(ex);
}
}
_reader.Close();
}
}
private static void ShowUsage()
{
Console.WriteLine("Usage");
Console.WriteLine("UserLinks [FilePath] [SharePoint URL]");
}
}
}
So problem solved & as a side benefit, this program can be used to force links to show up in a user's My Links list.
This post has some pretty good information about MyLinks and its relationship with the SSP database (that's actually where these links are stored counterintuitively.) Hopefully you can have your DBA validate that these links still exist; and that they're associated with the correct profiles.
http://www.k2distillery.com/2009/01/moving-sharepoint-my-links-between-ssps.html
When you do a profile import, you normally risk losing the existing customization/updated information.