today i tried to do a copy of a database(design and documents) by doing right click on database-> New copy... and i see that the new copy of database have smallest dimension of the source database (source database 900mb and the new copy 170mb).
I see also that if i access in the new copy of this database the size passes form 170mb to 400mb (i think is because notes create index...)
But i need to copy database only to create a backup so the size is important because i have to do daily backup and to consulted it only in particoular case.
So i built an agent to do this copy but the result is not the same, the new copy have the same dimension of the source database.
Is there a trick to obtain the same "compression" of right click on database-> New copy... but with script code ?
I post the code that i use but without that compression:
Dim Db As NotesDatabase
Dim Ws As New NotesUIWorkspace
Dim Session As New NotesSession
Dim DbDir As NotesDbDirectory
Dim AllDocs As NotesView
Dim SourceDb As NotesDatabase
Dim ArchiveDb As NotesDatabase
Dim SourceDoc As NotesDocument
Set Db=Session.CurrentDatabase
Set DbDir=Session.GetDbDirectory(Db.Server)
Set SourceDb = DbDir.GetFirstDatabase(DATABASE)
Do While Not SourceDb Is Nothing
Print"BackUp Database "+Cstr(SourceDb.FileName)
Gosub BackUpDatabase
Set SourceDb = DbDir.GetNextDatabase
Loop
Exit Sub
BackUpDatabase:
If SourceDb.IsOpen=False Then Call SourceDb.Open( "", "" )
Set ArchiveDb = SourceDb.CreateCopy( "", "C:\Test\"+Cstr(Day(Date))+"-"+Cstr(Month(Date))+"-"+Cstr(Year(Date))+"\"+SourceDb.FilePath)
Set AllDocs = SourceDb.CreateView( "#AllDocs", "SELECT #All" )
Set SourceDoc=AllDocs.GetFirstDocument
Do While Not SourceDoc Is Nothing
Call SourceDoc.CopyToDatabase(ArchiveDb)
Set SourceDoc=AllDocs.GetNextDocument(SourceDoc)
Loop
Call AllDocs.Remove()
Return
The "compression" as you call it is simply the "absence" of view- indices.
The database size consists of:
Size of all documents
Size of the design of the database
Empty Space (if data was deleted, but the database not compacted)
View indices
A new copy (via client) has NO indices, but as soon as you open a view, the size of the database will increase.
Your script is NOT a good idea for a backup:
All documents get a new Creation Stamp
All documents get a new replica id - Response- hierarchies will be completely lost after your copy
etc.
If you really want to go that direction, then you woould have to compact the database using a compact -D to get the free space and the view indices back,
but I would never do a backup of a production database like this...
Another (better) possibility would be to create a new replica with LotusScript- code and make a backup of that:
Set ArchiveDb = SourceDb.CreateReplica( "", "C:\Test\"+Cstr(Day(Date))+"-"+Cstr(Month(Date))+"-"+Cstr(Year(Date))+"\"+SourceDb.FilePath)
If you keep that database after doing the backup, then you might want to change the replica- id of the database using this code:
Option Public
Option Declare
Const wAPIModule = "NNOTES" ' Windows/32
Type API_TIMEDATE
lngInnards(1) As Long
End Type
Type API_DBREPLICAINFO
ID As API_TIMEDATE 'ID that is same for all replica files
intFlags As Integer 'Replication flags
intCutoffInterval As Integer 'Automatic Replication Cutoff
Cutoff As API_TIMEDATE 'Replication cutoff date
End Type
Declare Private Function NSFDbOpen Lib wAPIModule Alias "NSFDbOpen" _
( ByVal P As String, hDB As Long) As Integer
Declare Private Function NSFDbClose Lib wAPIModule Alias "NSFDbClose" _
( ByVal hDB As Long) As Integer
Declare Private Function NSFDbReplicaInfoGet Lib wAPIModule Alias "NSFDbReplicaInfoGet" _
(ByVal hdb As Long, hdbr As API_DBREPLICAINFO) As Integer
Declare Private Function NSFDbReplicaInfoSet Lib wAPIModule Alias "NSFDbReplicaInfoSet" _
(ByVal hdb As Long, hdbr As API_DBREPLICAINFO) As Integer
Sub ChangeReplicaID( strServer As String, strFilePath As String, strReplicaID As String )
Dim intRc As Integer
Dim lngDb As Long
Dim RepInfo As API_DBREPLICAINFO
If strServer = "" Then
intRc = NSFDbOpen( strFilePath, lngDb )
Else
intRc = NSFDbOpen( strServer & "!!" & strFilePath, lngDb )
End If
If intRc <> 0 Then
MessageBox "Could not open DB"
Exit Sub
End If
intRc = NSFDbReplicaInfoGet(lngDb, RepInfo)
If intRc <> 0 Then
MessageBox "Could not get replication info"
Exit Sub
End If
RepInfo.ID.lngInnards(1) = Val( "&H"+Left$( strReplicaID, 8 ) )
RepInfo.ID.lngInnards(0) = Val( "&H"+Right$( strReplicaID, 8 ) )
intRc = NSFDbReplicaInfoSet( lngDb, RepInfo) ' take a deep breath... :-)
intRc = NSFDbReplicaInfoGet( lngDb, RepInfo)
If intRc <> 0 Then
MessageBox "Could not get replication info after setting"
Else
MessageBox "Success"
End If
End Sub
But the best advice I can give: Use a professional Backup- Software to do that job.
Related
I would like to delete a mail database over a notes agent. Sometimes the mail databases have a replica.
Whats the best way to delete the mail file itself and also all other replicas (if they exist)?
My code below looks like this, but it doesn't delete the mail file in a replica.
Dim mailfile As String
mailfile = "mail\doe.nsf"
Dim db As New NotesDatabase("", mailfile)
If db.IsOpen Then
'Mark to delete it later
Call db.MarkForDelete()
Else
'Delete now
Call db.Remove
End If
You could use the built in function to do this by using NotesAdministrationProcess:
Sub Initialize
Dim session As New NotesSession
Dim adminp As NotesAdministrationProcess
Set adminp = _
session.CreateAdministrationProcess("Software_Server")
noteid$ = adminp.DeleteReplicas("Software_Server", "Guys1")
'- in case you want to open the generated adminp request
If noteid$ <> "" Then
Dim db As New NotesDatabase("Software_Server", "admin4")
Dim ws As New NotesUIWorkspace
Call ws.EditDocument(False, db.GetDocumentByID(noteid$))
End If
End Sub
If you do not want to wait for this (as it needs time: replicate admin4.nsf to all servers, execute admin process there, replicate back...), you can do this by yourself if you know the servers where the replicas are in advance:
Dim mailfile As String
mailfile = "mail\doe.nsf"
Dim otherServers(2) as String
Dim replicaID as String
Dim db as NotesDatabase
otherServers(0) = "FirstServerName/Certifier"
otherServers(1) = "SecondServerName/Certifier"
otherServers(2) = "ThirdServerName/Certifier"
Set db = New NotesDatabase("PrimaryServer/Certifier", mailfile)
If db.IsOpen Then
replicaID = db.ReplicaID
On Error Goto ErrorRemove
'Delete now
Call db.Remove
On Error Goto ErrorHandler
Forall serverName in otherServers
Set db = New NotesDatabase("", "")
Call db.OpenByReplicaID( serverName, replicaID )
If db.IsOpen Then
On Error Goto ErrorRemove
'Delete now
Call db.Remove
On Error Goto ErrorHandler
End If
End Forall
End If
EndOfRoutine:
Exit Sub
ErrorRemove:
Call db.MarkForDelete()
Resume Next
ErrorHandler:
'- do usual error handling here
REMARK: Your check for "db.IsOpen" does not help at all. As "IsOpen" does NOT return if a database is currently open somewhere. It returns, if YOUR SCRIPT was able to open the database at exaxtly that moment... I added an error handler to take this into account.
I've searched high and low and the following code is the closest I've come to my objective.
This is what I'm working on:
I wrote some code (OK, honestly, mostly copied bits and pieces and pasted into what is probably jumbled code that works) to email documents to my students. If a doc is open, I get and error, which allows me to manually save and close the doc (thx to Debug), and continue on. I would like to automate this, but Word seems to make things a tad difficult by opening each doc in a separate instance. I can get one instance and its doc, but if it is not the one I need, I cannot save and close it. I found how to get the other instances, but I have not found how to check each instance to see if the doc which it opened is the one I want.
I used ZeroKelvin's UDF in (Check if Word instance is running), which I modified a little bit...
Dim WMG As Object, Proc As Object
Set WMG = GetObject("winmgmts:")
For Each Proc In WMG.InstancesOf("win32_process")
If UCase(Trim(Proc.Name)) = "WINWORD.EXE" Then
*'Beginning of my code...*
*'This is what I need and have no idea how to go about*
Dim WdApp as Word.Application, WdDoc as Object
*' is it better to have WdDoc as Document?*
set WdDoc = ' ### I do not know what goes here ...
If WdDoc.Name = Doc2Send Or WdDoc.Name = Doc2SendFullName Then
*' ### ... or how to properly save and close*
WdApp.Documents(Doc2Send).Close (wdPromptToSaveChanges)
Exit For
End If
*'... end of my code*
Exit For
End If
Next 'Proc
Set WMG = Nothing
Thank you for your time and effort.
Cheers
You may like to consider controlling the number of instances of the Word application that are created. The function below, called from Excel, will return an existing instance of Word or create a new one only if none existed.
Private Function GetWord(ByRef WdApp As Word.Application) As Boolean
' 256
' return True if a new instance of Word was created
Const AppName As String = "Word.Application"
On Error Resume Next
Set WdApp = GetObject(, AppName)
If Err Then
Set WdApp = CreateObject(AppName, "")
End If
WdApp.Visible = True
GetWord = CBool(Err)
Err.Clear
End Function
The function is designed for early binding, meaning you need to add a reference to the Microsoft Word Object Library. During development it's better to work that way. You can change to late binding after your code has been fully developed and tested.
Please take note of the line WdApp.Visible = True. I added it to demonstrate that the object can be modified. A modification done within the If Err bracket would apply only to a newly created instance. Where I placed it it will apply regardless of how WdApp was created.
The next procedure demonstrates how the function might be used in your project. (You can run it as it is.)
Sub Test_GetWord()
' 256
Dim WdApp As Word.Application
Dim NewWord As Boolean
Dim MyDoc As Word.Document
NewWord = GetWord(WdApp)
If NewWord Then
Set MyDoc = WdApp.Documents.Add
MsgBox "A new instance of Word was created and" & vbCr & _
"a document added named " & MyDoc.Name
Else
MsgBox "Word is running and has " & WdApp.Documents.Count & " document open."
End If
End Sub
As you see, the variable WdApp is declared here and passed to the function. The function assigns an object to it and returns information whether that object previously existed or not. I use this info to close the instance if it was created or leave it open if the user had it open before the macro was run.
The two message boxes are for demonstration only. You can use the logical spaces they occupy to do other things. And, yes, I would prefer to assign each document in an instance I'm looking at to an object variable. While using early binding you will get the added benefit of Intellisense.
EDIT
Your procedure enumerates processes. I wasn't able to find a way to determine convert the process into an instance of the application. In other words, you can enumerate the processes and find how many instances of Word are running but I can't convert any of these instances into a particular, functioning instance of the application so as to access the documents open in it. Therefore I decided to enumerate the windows instead and work from there back to the document. The function below specifically omits documents opened invisibly.
Option Explicit
Private Declare PtrSafe Function apiGetClassName Lib "user32" Alias _
"GetClassNameA" (ByVal Hwnd As Long, _
ByVal lpClassname As String, _
ByVal nMaxCount As Long) As Long
Private Declare PtrSafe Function apiGetDesktopWindow Lib "user32" Alias _
"GetDesktopWindow" () As Long
Private Declare PtrSafe Function apiGetWindow Lib "user32" Alias _
"GetWindow" (ByVal Hwnd As Long, _
ByVal wCmd As Long) As Long
Private Declare PtrSafe Function apiGetWindowLong Lib "user32" Alias _
"GetWindowLongA" (ByVal Hwnd As Long, ByVal _
nIndex As Long) As Long
Private Declare PtrSafe Function apiGetWindowText Lib "user32" Alias _
"GetWindowTextA" (ByVal Hwnd As Long, ByVal _
lpString As String, ByVal aint As Long) As Long
Private Const mcGWCHILD = 5
Private Const mcGWHWNDNEXT = 2
Private Const mcGWLSTYLE = (-16)
Private Const mcWSVISIBLE = &H10000000
Private Const mconMAXLEN = 255
Sub ListName()
' 256
' adapted from
' https://www.extendoffice.com/documents/excel/4789-excel-vba-list-all-open-applications.html
Dim xStr As String
Dim xStrLen As Long
Dim xHandle As Long
Dim xHandleStr As String
Dim xHandleLen As Long
Dim xHandleStyle As Long
Dim WdDoc As Word.Document
Dim Sp() As String
On Error Resume Next
xHandle = apiGetWindow(apiGetDesktopWindow(), mcGWCHILD)
Do While xHandle <> 0
xStr = String$(mconMAXLEN - 1, 0)
xStrLen = apiGetWindowText(xHandle, xStr, mconMAXLEN)
If xStrLen > 0 Then
xStr = Left$(xStr, xStrLen)
xHandleStyle = apiGetWindowLong(xHandle, mcGWLSTYLE)
If xHandleStyle And mcWSVISIBLE Then
Sp = Split(xStr, "-")
If Trim(Sp(UBound(Sp))) = "Word" Then
ReDim Preserve Sp(UBound(Sp) - 1)
xStr = Trim(Join(Sp, "-"))
Set WdDoc = Word.Application.Documents(xStr)
' this applies if the document was not saved:-
If WdDoc.Name <> xStr Then Set WdDoc = GetObject(xStr)
Debug.Print xStr,
Debug.Print WdDoc.Name
End If
End If
End If
xHandle = apiGetWindow(xHandle, mcGWHWNDNEXT)
Loop
End Sub
Note that it's important to have the API functions at the top of the module - no code above them. Your question doesn't extend to what you want to do with the files but you wanted them listed, and that is accomplished.
We all know that ms access is not multitreaded so when msacces runs a long query it hangs waiting to the query to be completed. what i want is open from access an new instance of access to run a query or run vba code in background. after running it needs to kit itself after it turns back the results (maybe though the sql server background)
i have seen something before in excel but i wonder if it is posible to do in access
the excel variant is here [excel swarm][1
UPDate
i open access with the folowing code
Dim appAccess As Object
Set appAccess = CreateObject("Access.Application")
Call appAccess.OpenCurrentDatabase( _ "D:\test.accdb")
appAccess.UserControl = True
Set appAccess = Nothing
the target access db is preformatted with a loop as test with is started when access opens. the problem is that the souce access hangs during starting ans running of the target access.
i can use the timer to give it a delayed start and then its working.
the main problem is how can i stat a not preformated access db, create things like vba code, querys odbc connections etc and run it without the source db being hanging.
you could try something like this from a shell command to another VBA host, say excel, which could trigger the event. This is a class, where the properties of DB path and Query name are passed in, then GO is executed, it uses the Execute Complete event of the DBs ADO connection, I've coded it to create an Excel instance and populate with the results.
Ive not tested this fully as in the middle of something, but i'll test fully at lunch and edit as req'd, but a starting point
Option Explicit
Private WithEvents c As ADODB.Connection
Private strDBPath As String
Private strQueryToRun As String
Public Property Let DBPath(strPath As String)
strDBPath = strPath
End Property
Public Property Let QueryToRun(strQuery As String)
strQueryToRun = strQuery
End Property
Public Function GO()
Dim a As New Access.Application
a.OpenCurrentDatabase strDBPath, False
Set c = a.CurrentProject.Connection
c.EXECUTE strQueryToRun
a.CloseCurrentDatabase
a.Quit
Set a = Nothing
End Function
Private Sub c_ExecuteComplete(ByVal RecordsAffected As Long, ByVal pError As ADODB.Error, adStatus As ADODB.EventStatusEnum, _
ByVal pCommand As ADODB.Command, ByVal pRecordset As ADODB.Recordset, ByVal pConnection As ADODB.Connection)
' what to do with the results?
Dim xl As New Excel.Application
Dim xlWb As Excel.Workbook
xl.Visible = True
Set xlWb = xl.Workbooks.Add
xlWb.Sheets(1).Range("a1").CopyFromRecordset pRecordset
End Sub
Yes, that is possible.
Use command Shell to open another instance of Access - and add command line parameters Access to hold info about which queries to run.
I have around 50 lotus notes databases which I need to search through (including RTFs and attachments) based on a rather complicated query (for example: If customer = a or b or c or d or e ... or z & product = x or y) then == match; and a tag to (e.g. tag = found or not found)
However, I wasn't sure the best way to go about this and wanted to check three options.
Using Lotus' 'Search in View' should, after indexing, search all the databases - however I am unsure if it will accept a long, complicated search query
Coding an agent in Lotus SCRIPT to basically perform the search in 1. but this might be a way of getting it to accept a complicated query
Using external software (e.g. X1 Professional Search) to search outside of Lotus Notes (however I am not sure if I will be able to tag the files if I identify them in windows explorer).
Edit: My idea is:
Sub Initialise
Dim session as New NotesSession
Dim db as NotesDatabase
Dim dc as NotesDocumentCollection
Dim doc as NotesDocument
Dim searcy_query$
'On current database
Set db = session.CurrentDatabase
'Force index of database
Call db.UpdateFTIndex(True)
'Construct QUERY
search_query = "(Customer1|Customer2) & (Product1|Product2)"
'Search
Set dc = db.FTSearch(query, 0)
If dc.Count=0 Then
Msgbox "No matches!"
Exit Sub
End If
‘Tag the matched documents with “Flag”
Call dc.StampAll("Flag","Active)")
End Sub
However, I am not sure if this will return all matches and also regarding #TRIM and #UPPER (where to put them into the query as I will be searching all fields and RTFs rather than specific ones)
First of all: Searching more than one database at a time can of course be done "manually" by searching each individual database, collection the results and find a way to present the documents "somehow" (what will not be easy, as documents from different databases cannot be shown in "one" view - you would need to use "shadow- documents" or a web- approach (e.g. XPages))
BUT: Lotus Notes has a built-in function to do this, it is called "Domain Indexer". You can read more about how to setup "domain index" in this IBM Link or in your administration help.
Examples for using the domain- index for multi- database- searches can be found in catalog.nsf in the form "DomainQuery".
The search- string does not have a limit as far as I know, so that you can do very complex searches with this technique and it gives you all the matches.
If you search for a LotusScript- Solution, check the documentation for NotesDatabase.ftdomainsearch for example code like this (taken from developer help):
The following code, when placed in a button on a search form within a directory catalog database, searches the directory for the specified query string, and returns all results.
Sub Click(Source As Button)
Dim s As New NotesSession
Dim db As NotesDatabase
Dim w As New NotesUIWorkspace
Dim uidoc As NotesUIDocument
Dim doc as NotesDocument
Dim quer As String
Dim srdoc as NotesDocument
Set db=s.CurrentDatabase
Set uidoc = w.currentdocument
uidoc.refresh
Set doc=uidoc.Document
quer=doc.query(0)
On Error Resume Next
If db.isopen Then
If Err <> 0 Then
Messagebox STR_DBOPEN_ERROR, 0 , STR_ERROR
Err = 0
Exit Sub
End If
Set srdoc = db.ftdomainsearch(quer, 0)
If Err <> 0 Then
Messagebox STR_NDF , 0 , STR_LOTUS_NOTES
Err=0
Exit Sub
End If
srdoc.Form="SearchResults"
Call w.EditDocument(False, srdoc, True)
End If
End Sub
If you also intend to search Rich Text, the simplier way is Full Text Search, you got the point!
The syntax for your query is:
generic_text or ([_CreationDate]<07.10.2014 and ([CustomerFieldName]="Smith" or [CustomerFieldName]="Wesson"))
For formating the Result, I'ld suggest looking at:AppendDocLink for something like:
Dim session As New NotesSession
Dim db As New NotesDatabase("", "resu.nsf")
Dim newDoc As NotesDocument
Dim rtitem As NotesRichTextItem
Dim dc As NotesDocumentCollection
Dim doc As NotesDocument
Set newDoc = New NotesDocument( db )
Set rtitem = New NotesRichTextItem( newDoc, "Body" )
'you have to loop on your 50 DBs
'Search =>from your code
Set dc = db.FTSearch(query, 0)
while not doc is nothing
Call rtitem.AppendDocLink( doc, db.Title )
Call rtitem.AddTab( 1 )
Call rtitem.AppendText( doc.FieldImportantInResult( 0 ) )
Call rtitem.AddNewLine( 1 )
Set doc = dc.GetNextDocument( doc )
Wend
newDoc.save true, true
'this part is not to add it you plane to run your search in background
Dim w As New NotesUIWorkspace
Call w.EditDocument(False, newDoc, True)
I have some NotesDocument where some RichText fields have both text and inline images. I can get text part of that items but can't retrieve inline images using lotusscript. Could any one please suggest me a way to retrieve inline images from that documents.
LotusScript code:
Sub Click(Source As Button)
Dim session As New NotesSession
Dim db As NotesDatabase
Dim mainDoc As NotesDocument
Dim v As NotesView
Set db = session.CurrentDatabase
Dim fileName As String
Dim fileNum As Integer
fileNum% = Freefile()
fileName$ = "D:\data.txt"
Open FileName$ For Append As fileNum%
Set v = db.GetView("MyView")
Set mainDoc = v.GetFirstDocument
While Not ( mainDoc Is Nothing )
Forall i In mainDoc.Items
If i.Type = RICHTEXT Then
Write #fileNum% , i.Name & ":" & i.text 'how the images??
End If
End Forall
Set mainDoc = v.GetNextDocument( mainDoc )
Wend
End Sub
Thanks.
Midas is the easiest way to do it, but it isn't free. (It's more than worth the money in overall time saved, but if your organisation is anything like the ones I've worked for, the entire cost of the tool is going to be foisted off on the billing unit that owns the current project, rather than have it amortized over the entire org, and they're likely to change their requirements before agreeing to the cost.) There is another approach, and that's to export the database to DXL (Domino XML) using the export option ConvertNotesBitmapToGIF. The images will turn up in the XML as <picture> elements with the data Base64-encoded. If you're operating entirely within the Notes environment, you'll need to create a temporary document with a rich text field used as a NotesMIMEEntity to transform the encoded picture to binary before streaming it to a file (using NotesStream). All of this assumes that you are working with version 6 or higher; if you are on R5 or earlier, Midas or directly accessing the CD records using the C API are the only way to fly.
Seven years later, and I've been pulling my hair out over this one. Rod H's answer is for attachments, but embedded images are another thing entirely.
My best luck came from #andre-guirard's LotusScript Gold Collection code located here: https://www.openntf.org/main.nsf/project.xsp?r=project/LotusScript%20Gold%20Collection However, that doesn't get everything because it doesn't handle documents where the embedded images were embedded the old way. (Notes changed the way it stores embedded images.)
I tried very hard to combine it with AGECOM's information presented here: https://www.agecom.com.au/support/agecomkb.nsf/0/58cbf10f0ab723c9ca25803e006c7de8?OpenDocument by changing Andre's EmbeddedImage object to seamlessly handle both formats by looking to see if the embedded image within a rich text field is really just a pointer to a $FILE field and then, if so, getting a FileItem object, but eventually I exhausted my understanding and options to a degree that I couldn't justify spending my employer's resources (my time) on it.
So if you have embedded images that are all contained in the new way, I think Andre's code will work unmolested. Otherwise, I tried my best but I don't have an answer... I have what is (for me) a dead end, presented in the hopes that you or someone else who stumbles upon it can embarrass me by explaining what I was doing wrong!
Basically, I started with Andre's code and changed it in the following ways...
In DOMUtils, add the following method:
%REM
Function DU_GetMeOrNextSiblingWithAttr
Description: Starting with a particular node, return that node or the next sibling with an attribute that has a particular value.
Does not recurse into the tree; looks only at the node passed and later siblings.
Parameters:
nodeStart: node to start your search with.
targetElement: element name of desired node.
attrName: attribute name you want to check.
attrValue: attribute value of element you're looking for.
flags: string-matching flags to compare attribute, e.g. 1 for case insensitive.
%END REM
Function DU_GetMeOrNextSiblingWithAttr(nodeStart As NotesDOMNode, ByVal targetElement$, ByVal attrName$, ByVal attrValue$, ByVal flags%) As NotesDOMElementNode
Dim node As NotesDOMNode, elTmp As NotesDOMElementNode
Set node = nodeStart
Do Until node.Isnull
If node.Nodetype = DOMNODETYPE_ELEMENT_NODE Then
If node.Nodename = targetElement Then
Set elTmp = node
If StrComp(elTmp.Getattribute(attrName), attrValue, flags) = 0 Then
Set DU_GetMeOrNextSiblingWithAttr = elTmp
Exit Function
End If
End If
End If
Set node = node.Nextsibling
Loop
End Function
Replace FileItem.New with the following code:
%REM
Sub New
Description: Arguments are the parsed DOM node of the element representing a
design element, and the name of the composite item you would like to read,
modify or create.
%END REM
Sub New(parent As FileItemParent, elNote As NotesDOMElementNode, itemName$, fileName$)
Set m_elNote = elNote
SetItem elNote, itemName$, fileName$
Dim node As NotesDOMNode
Set node = m_elNote.Parentnode
While node.Nodetype <> DOMNODETYPE_DOCUMENT_NODE
Set node = node.Parentnode
Wend
Set m_domd = node
parent.RegisterFileItem Me ' make sure the design element knows about us.
' (in case someone gets smart and invokes the constructor directly
' instead of using the nice methods we've provided).
End Sub
%REM
Sub SetItem
<!-- Created Dec 6, 2017 by JSmart523 -->
If fileName$ is blank, returns the XPath equivalent of elNote/ancestor::document/item[#name=itemName$][position()=1]
If fileName$ is not blank, returns the XPath equivalent of elNote/ancestor::document/item[#name=itemName$][object/file/#name=fileName$][position()=1]
Case insensitive. Changes itemName$ and fileName$ to the correct case if found.
Also sets Me.m_elItem to the returned NotesDOMElementNode
Also sets Me.m_elRawData to the file contents
%END REM
Sub SetItem(elNote As NotesDOMElementNode, itemName$, fileName$)
Dim elFile As NotesDOMElementNode
Dim node As NotesDOMNode
'set node to ancestor::document
Set node = elNote
Do Until node.NodeName = "document"
Set node = node.ParentNode
Loop
'If fileName$ = "", get the first ancestor::document/item[#name=itemName$]
'Otherwise, get the first ancestor::document/item[#name=itemName$][/object/file/#name=fileName$]
Set m_elItem = DU_GetChildOfType(node, DOMNODETYPE_ELEMENT_NODE)
QualifyingItem m_elItem, itemName$, m_elRawData, fileName$
m_itemName = itemName$
m_fileName = fileName$
End Sub
%REM
Sub QualifyingItem
<!-- Created Dec 8, 2017 by JSmart523 -->
Starting with incoming elItem node, ensures it's an item we want or changes elItem to the first sibling that qualifies.
%END REM
Sub QualifyingItem(elItem As NotesDOMElementNode, itemName$, elRawData As NotesDOMElementNode, fileName$)
Dim elFile As NotesDOMElementNode
Dim node As NotesDOMNode
Dim elObject As NotesDOMElementNode
If Not elItem Is Nothing Then
'Initially, elItem is just a starting point, not necessarily the item we want.
'If it's an item with the right name, great, otherwise change elItem to the next sibling item with the right name.
Set elItem = DU_GetMeOrNextSiblingWithAttr(elItem, "item", "name", itemName$, 1)
If Not elItem Is Nothing Then
If fileName$ = "" Then
'we have the right item, and aren't looking for a file node, which means we want the rawitemdata node
Set elRawData = DU_getChildNamed("rawitemdata", elItem)
Else
'We are looking for a $FILE item that contains a file.
'There are possibly several $FILE items within a document, one for each file. We've got the right one if ./object/file/#name = fileName$
Do
Set elObject = DU_GetChildNamed("object", elItem)
If Not elObject Is Nothing Then
Set elFile = DU_GetChildWithAttr(elObject, "file", "name", fileName$, 1)
If Not elFile Is Nothing Then
'Yay! We have the right elItem node!
Set elRawData = DU_GetChildNamed("filedata", elFile)
fileName$ = elFile.GetAttribute("name")
Exit Do
End If
End If
Set elItem = DU_GetMeOrNextSiblingWithAttr(elItem.NextSibling, "item", "name", itemName$, 1)
Loop Until elItem Is Nothing
'At this point, either we jumped out of the loop with a valid elItem and elRawData, or elItem is Nothing
End If
End If
End If
If elItem Is Nothing Then
'we didn't find the correct item
'make sure elRawData is changed to Nothing, too.
Set elRawData = Nothing
Else
itemName$ = elItem.GetAttribute("name")
End If
End Sub
Also in FileItem script library, add a new class, FileItemParent
%REM
Class FileItemParent
<!-- Created Dec 5, 2017 by JSmart523 -->
This is a base class for objects that use FileItem objects
%END REM
Class FileItemParent
m_elElRoot As NotesDOMElementNode
m_elFD As NotesDOMElementNode
Public m_fileItem As FileItem
m_fItems List As FileItem ' list of FileItems we've created and returned to caller.
m_iMode As Integer
%REM
Property Get DOMElement
Description: Return the element node representing the design element.
%END REM
Public Property Get DOMElement As NotesDOMElementNode
Set DOMElement = m_elElRoot
End Property
%REM
Sub New
Arguments:
db: the database containing the design element.
elElement: the DOM element corresponding to the design note (e.g. the <note>
element).
domp: The DOM parser object containing elElement.
%END REM
Sub New(elElement As NotesDOMElementNode)
Set m_elElRoot = elElement
End Sub
Sub Delete
On Error Resume Next
ForAll thing In m_fItems
Delete thing
End ForAll
End Sub
%REM
Function HasItem
Description: Determine whether there's an item element in the note DXL with a
given item name.
Note that the presence of an item doesn't guarantee it's formatted as a file
CD record.
%END REM
Function HasItem(ByVal itemName$) As Boolean
HasItem = Not (DU_GetChildWithAttr(m_elElRoot, "item", "name", itemName, 1) Is Nothing)
End Function
%REM
Function RegisterFileItem
Description: For internal use -- lets the FileItem class notify us that it's
referencing our DOM tree so that we can delete the object if we erase the
corresponding item element.
%END REM
Sub RegisterFileItem(x As FileItem)
Set m_fItems(LCase(x.itemName)) = x
If m_FileItem Is Nothing Then
Set m_FileItem = x
End If
End Sub
%REM
Function GetFileItem
Description: Retrieve the FileItem object associated with a CD-record item.
An object will be returned even if the item doesn't exist, which you can
use to create the item via UpdateFile method.
%END REM
Function GetFileItem(itemName$, fileName$) As FileItem
Set GetFileItem = New FileItem(Me, m_elElRoot, itemName, fileName)
End Function
End Class
The FileItemParent class is primarily code taken from Andre's FileResource class so that both FileResource and EmbeddedImage can use it. Change FileResource to extend FileItemParent, removing any duplicated code.
Now we want to change EmbeddedImage so that, even if the embedded image node contains a link to a $FILE item rather than the actual contents, return the actual contents.
So, change EmbeddedImage to extend FileItemParent
Add/replace the following methods to EmbededImage
%REM
Sub InitFileItem
<!-- Created Dec 6, 2017 by JSmart523 -->
Called by New
%END REM
Sub InitFileItem()
Dim buffer As Variant 'byte array
Dim iFileNameLen As Integer
Dim sFileName As String
Dim sItemName As String
Dim stream As NotesStream
If Len(m_b64) < 30000 Then
'If content is short then maybe it's a link to a $FILE item instead of the actual content?
Dim session As New NotesSession
Set stream = session.CreateStream()
Base64ToBinary m_b64, stream
stream.Position = 0
buffer = stream.Read(1)
If buffer(0) = 196 Then
'this is a link to a $FILE, not the actual image contents!
stream.Position = 10
buffer = stream.Read(2)
iFileNameLen = ConvertWordByteArray(buffer)
stream.Position = 24
buffer = stream.Read(iFileNameLen)
sFileName = BytesToString(buffer)
sItemName = "$FILE"
GetFileItem sItemName, sFileName 'sets m_fileItem to a FileItem object
End If
End If
End Sub
%REM
Property Get SuggestedFileName
%END REM
Public Property Get SuggestedFileName As String
If m_fileItem Is Nothing Then
SuggestedFileName = "Embedded-" + ItemName + "." + SuggestedFileType
Else
SuggestedFileName = m_fileItem.FileName
If InStr(SuggestedFileName, ".") = 0 Then
SuggestedFileName = SuggestedFileName + "." + SuggestedFileType
End If
End If
End Property
%REM
Property Get SuggestedFileType
%END REM
Public Property Get SuggestedFileType As String
If ImageType = "notesbitmap" Then
SuggestedFileType = "bmp"
Else
SuggestedFileType = ImageType
End If
End Property
%REM
Sub ReadFileToStream
%END REM
Sub ReadFileToStream(streamOut As NotesStream)
If m_FileItem Is Nothing Then
ReadToStream streamOut
Else
Set m_FileItem.Stream = streamOut
m_FileItem.Load
End If
End Sub
and then change EmbeddedItem.New to, at the end, call InitFileItem so that if it's a link then getting the contents returns the contents rather than the link.
Okay, so far so good as far as I know, but the problem is that CD Records of embedded images stored within $FILE items (i.e. the rich text field's embedded image node simply contains a link rather than the actual image) are/were documented in a way that was, for me, impenetrable, despite AGECOM's code and explanations. I could use the above code and Andre's EmbeddedImageList object to grab every single embedded image but I simply couldn't get a "ConvertOldCDToNew" method working so I couldn't convert the old CD Record format into solid, uncorrupted files! I don't know if I was stripping too many bytes, not stripping the right ones, or maybe I just forgot to carry the two!
I suggest you look at the Genii Software MidasLSX product. They offer a package of LotusScript extensions that make it easier to deal with the complexities of Lotus Notes Rich Text items.
http://www.geniisoft.com/showcase.nsf/MidasHelp
Otherwise, you can experiment with the NotesRichTextNavigator class to gain access to the image in the rich text item (in theory). There is very little documentation on this sort of thing. I couldn't quite tell what an image would appear as using that class, but assuming you navigate through the rich text item and are able to get a handle the image as a NotesEmbeddedObject, I know there's a way to save the object to disk from that class.
Another (crazy) thought is to email the document, and have it received by another program that can more easily process the body of the email. Notes just isn't very helpful with processing its own rich text fields.
Here is an agent I use to detach files from a richtext field on my documents.
Option Public
Dim uidoc As notesuidocument
Dim doc As NotesDocument
Dim db As NotesDatabase
Dim obj As NotesEmbeddedObject
Dim collection As NotesDocumentCollection
Dim rt As Variant
Dim attachNames As Variant
Dim i As Integer, x As Integer
Dim j As Integer
' Agent - Detach Attachments to C drive for later reattachment
' Copies all attachment in richtext field to personal directory.
Sub Initialize
Dim ws As New notesuiworkspace
Dim ses As New NotesSession
Set db = ses.CurrentDatabase
Set collection = db.UnprocessedDocuments
' get first doc in collection
For j = 1 To collection.Count
Set doc = collection.GetNthDocument( j )
' --- create array of filenames of all the attachments in the document
i = 0
Redim attachNames(i)
Forall x In doc.items
If x.name = "$FILE" Then
attachNames(i) = x.values(0)
i = i + 1
Redim Preserve attachNames(i)
End If
End Forall
If i > 0 Then
Redim Preserve attachNames(i-1)
End If
' --- for all of the filenames in attachNames, if it exists in the rich text field, detatch them
If doc.hasItem("richtextfieldname") Then
Set rt = doc.GetFirstItem("richtextfieldname")
End If
If attachNames(0) <> "" Then
Forall x In attachNames
Set obj = rt.GetEmbeddedObject( x )
If Not( obj Is Nothing ) Then
Call obj.ExtractFile( "C:\path\" & Cstr(x) )
End If
End Forall
End If
Call doc.save(True, False)
Next
End Sub