--- /dev/null
+// installer.cpp : Defines the entry point for the console application.
+//
+
+#include "stdafx.h"
+#include <stdio.h>
+#include <stdlib.h>
+
+// Turn off the warnings nagging you to use the more complicated *_s
+// "secure" functions that are actually more difficult to use securely.
+#pragma warning(disable:4996)
+
+#define WIN32_LEAN_AND_MEAN
+#include <windows.h>
+#include <wtypes.h>
+#include <objbase.h>
+#include <shldisp.h>
+
+int wmain(int argc, wchar_t *argv[]);
+wchar_t * get_default_destination_dir();
+void self_extract(wchar_t *destination_dir);
+void unzip_from_executable(wchar_t *executable_path, wchar_t *destination_dir);
+size_t read_uint32_le(unsigned char *b);
+void unzip(wchar_t *zip_path, wchar_t *destination_dir);
+bool have_acceptable_python();
+void install_python(wchar_t *python_installer_dir);
+
+#define fail_unless(x, s) if (!(x)) { fail(s); }
+void fail(char *s);
+
+#define MINIMUM_PYTHON_VERSION L"2.7.0"
+#define INSTALL_PYTHON_VERSION L"2.7.8"
+#define PYTHON_INSTALLER_32BIT (L"python-" INSTALL_PYTHON_VERSION L".msi")
+#define PYTHON_INSTALLER_64BIT (L"python-" INSTALL_PYTHON_VERSION L".amd64.msi")
+
+void noop_handler(const wchar_t * expression,
+ const wchar_t * function,
+ const wchar_t * file,
+ unsigned int line,
+ uintptr_t pReserved) {
+}
+
+int wmain(int argc, wchar_t *argv[]) {
+ _set_invalid_parameter_handler(noop_handler);
+
+ if (argc >= 2 && wcscmp(argv[1], L"--help") == 0) {
+ printf("installer <destination_dir>\n");
+ }
+ wchar_t *destination_dir = (argc >= 2) ? argv[1] : get_default_destination_dir();
+
+ self_extract(destination_dir);
+ //unzip(L"C:\\tahoe\\allmydata-tahoe-1.10.0c1.zip", destination_dir);
+
+ if (!have_acceptable_python()) {
+ install_python(destination_dir);
+ }
+ //unlink(python_installer);
+ return 0;
+}
+
+wchar_t * get_default_destination_dir() {
+ // TODO: get Program Files directory from the registry
+ return L"C:\\tahoe\\windowstest";
+}
+
+void self_extract(wchar_t *destination_dir) {
+ wchar_t executable_path[MAX_PATH];
+
+ HMODULE hModule = GetModuleHandle(NULL);
+ fail_unless(hModule != NULL, "Could not get the module handle.");
+ GetModuleFileNameW(hModule, executable_path, MAX_PATH);
+ fail_unless(GetLastError() == ERROR_SUCCESS, "Could not get the path of the current executable.");
+
+ unzip_from_executable(executable_path, destination_dir);
+}
+
+void unzip_from_executable(wchar_t *executable_path, wchar_t *destination_dir) {
+ // shell32's zipped folder implementation is strict about the zip format and
+ // does not support unzipping a self-extracting exe directly. So we copy the
+ // original zip file that was appended to the exe to a temporary directory,
+ // and use shell32 to unzip it from there. To get the length of the zip file,
+ // we look at its "end of central directory record", which is documented at
+ // <http://www.pkware.com/documents/casestudies/APPNOTE.TXT>.
+ // For simplicity we only handle the case of a zip file that has no archive
+ // comment, that does not use disk spanning, and that does not have a
+ // "Zip64 end of central directory record".
+
+ // APPNOTE.TXT section 4.3.16.
+ const size_t sizeof_eocd = 22;
+ unsigned char end_data[sizeof_eocd];
+ unsigned char eocd_signature[] = {0x50, 0x4B, 0x05, 0x06};
+ unsigned char comment_length[] = {0x00, 0x00};
+ unsigned char disk_num[] = {0x00, 0x00};
+
+ errno = 0;
+ FILE *f = _wfopen(L"C:\\tahoe\\foo.zip", L"rb");
+ fail_unless(f != NULL && errno == 0 && ferror(f) == 0,
+ "Could not open executable file.");
+
+ fseek(f, -(off_t) sizeof_eocd, SEEK_END);
+ fail_unless(errno == 0 && ferror(f) == 0,
+ "Could not seek to end-of-central-directory record.");
+
+ __int64 eocd_offset = _ftelli64(f);
+ fail_unless(errno == 0 && ferror(f) == 0 && eocd_offset >= 0,
+ "Could not read position of end-of-central-directory record.");
+ fail_unless(eocd_offset + sizeof_eocd <= 0xFFFFFFFFi64,
+ "Cannot read an executable file >= 4 GiB.");
+
+ size_t n = fread(end_data, sizeof(end_data), 1, f);
+ fail_unless(n == 1 && errno == 0 && ferror(f) == 0,
+ "Could not read end records.");
+
+ fail_unless(memcmp(end_data + sizeof(end_data) - sizeof(comment_length),
+ comment_length, sizeof(comment_length)) == 0,
+ "Cannot read a zip file that has an archive comment.");
+
+ fail_unless(memcmp(end_data, eocd_signature, sizeof(eocd_signature)) == 0,
+ "Could not find the end-of-central-directory signature.");
+
+ fail_unless(memcmp(end_data + 4, disk_num, sizeof(disk_num)) == 0 &&
+ memcmp(end_data + 6, disk_num, sizeof(disk_num)) == 0,
+ "Cannot read a zipfile that spans disks.");
+
+ size_t cd_length = read_uint32_le(end_data + 12);
+ size_t cd_offset = read_uint32_le(end_data + 16);
+ __int64 zip_length = cd_offset + cd_length + sizeof_eocd;
+ fail_unless(zip_length <= 0x7FFFFFFFi64,
+ "Cannot copy a zip file >= 2 GiB.");
+ fseek(f, -(off_t) zip_length, SEEK_END);
+ fail_unless(errno == 0 && ferror(f) == 0,
+ "Could not seek to start of embedded zip file.");
+
+ const wchar_t tmp_filename[] = L"tahoe-lafs.zip"; // FIXME make this more unique.
+ wchar_t tmp_path[MAX_PATH];
+ DWORD len = GetTempPathW(MAX_PATH, tmp_path);
+ fail_unless(len > 0 && len < MAX_PATH - wcslen(tmp_filename),
+ "Could not obtain temporary directory path.");
+ wcscpy(tmp_path + len, tmp_filename);
+
+ FILE *tmp_file = _wfopen(tmp_path, L"wb");
+ unsigned char buf[16384];
+ size_t remaining_length = (size_t) zip_length;
+ while (remaining_length > 0) {
+ size_t chunk_length = min(remaining_length, sizeof(buf));
+ n = fread(buf, chunk_length, 1, f);
+ fail_unless(n == 1 && errno == 0 && ferror(f) == 0,
+ "Could not read from executable file.");
+ fwrite(buf, chunk_length, 1, tmp_file);
+ fail_unless(n == 1 && errno == 0 && ferror(f) == 0,
+ "Could not write to temporary file.");
+ remaining_length -= chunk_length;
+ }
+ fclose(tmp_file);
+
+ unzip(tmp_path, destination_dir);
+}
+
+// read unsigned little-endian 32-bit integer
+size_t read_uint32_le(unsigned char *b) {
+ return ((size_t) b[0] ) |
+ ((size_t) b[1] << 8) |
+ ((size_t) b[2] << 16) |
+ ((size_t) b[3] << 24);
+}
+
+void unzip(wchar_t *zip_path, wchar_t *destination_dir) {
+ // Based on <https://social.msdn.microsoft.com/Forums/vstudio/en-US/45668d18-2840-4887-87e1-4085201f4103/visual-c-to-unzip-a-zip-file-to-a-specific-directory?forum=vclanguage>
+
+ wprintf(L"Extracting %ls\nto %ls\n", zip_path, destination_dir);
+
+ // SysAllocString: <http://msdn.microsoft.com/en-gb/library/windows/desktop/ms221458(v=vs.85).aspx>
+ // BSTR: <http://msdn.microsoft.com/en-us/library/windows/desktop/ms221069(v=vs.85).aspx>
+
+ VARIANT zip_path_var;
+ zip_path_var.vt = VT_BSTR;
+ zip_path_var.bstrVal = SysAllocString(zip_path);
+ fail_unless(zip_path_var.bstrVal != NULL, "Could not allocate string for zip file path.");
+
+ VARIANT destination_dir_var;
+ destination_dir_var.vt = VT_BSTR;
+ destination_dir_var.bstrVal = SysAllocString(destination_dir);
+ fail_unless(destination_dir_var.bstrVal != NULL, "Could not allocate string for destination directory path.");
+
+ // CoInitializeEx: <http://msdn.microsoft.com/en-gb/library/windows/desktop/ms695279(v=vs.85).aspx>
+ HRESULT res = CoInitializeEx(NULL, 0);
+ fail_unless(res == S_OK || res == S_FALSE, "Could not initialize COM.");
+
+ // CoCreateInstance: <http://msdn.microsoft.com/en-gb/library/windows/desktop/ms686615(v=vs.85).aspx>
+ IShellDispatch *shell;
+ res = CoCreateInstance(CLSID_Shell, NULL, CLSCTX_INPROC_SERVER, IID_IShellDispatch, (void **) &shell);
+ fail_unless(res == S_OK, "Could not create Shell instance.");
+
+ // Folder.NameSpace: <http://msdn.microsoft.com/en-gb/library/windows/desktop/gg537721(v=vs.85).aspx>
+ Folder *zip_folder = NULL;
+ res = shell->NameSpace(zip_path_var, &zip_folder);
+ fail_unless(res == S_OK && zip_folder != NULL, "Could not create zip Folder object.");
+
+ Folder *destination_folder = NULL;
+ res = shell->NameSpace(destination_dir_var, &destination_folder);
+ fail_unless(res == S_OK && destination_folder != NULL, "Could not create destination Folder object.");
+
+ FolderItems *zip_folderitems = NULL;
+ zip_folder->Items(&zip_folderitems);
+ fail_unless(zip_folderitems != NULL, "Could not create zip FolderItems object.");
+
+ long files_count = 0;
+ zip_folderitems->get_Count(&files_count);
+ printf("count %d\n", files_count);
+
+ VARIANT zip_idispatch_var;
+ zip_idispatch_var.vt = VT_DISPATCH;
+ zip_idispatch_var.pdispVal = NULL;
+ zip_folderitems->QueryInterface(IID_IDispatch, (void **) &zip_idispatch_var.pdispVal);
+ fail_unless(zip_idispatch_var.pdispVal != NULL, "Could not create IDispatch for zip FolderItems object.");
+
+ // Folder.CopyHere: <http://msdn.microsoft.com/en-us/library/ms723207(v=vs.85).aspx>
+ // (4) Do not display a progress dialog box.
+ // (16) Respond with "Yes to All" for any dialog box that is displayed.
+ // (256) Display a progress dialog box but do not show the file names.
+ // (512) Do not confirm the creation of a new directory if the operation requires one to be created.
+ // (1024) Do not display a user interface if an error occurs.
+ VARIANT options_var;
+ options_var.vt = VT_I4;
+ options_var.lVal = 16 | 256 | 512 | 1024;
+
+ res = destination_folder->CopyHere(zip_idispatch_var, options_var);
+ fail_unless(res == S_OK, "Could not extract zip file contents to destination directory.");
+
+ // We don't bother to free/release stuff unless we succeed, since we exit on failure.
+
+ // SysFreeString: <http://msdn.microsoft.com/en-gb/library/windows/desktop/ms221481(v=vs.85).aspx>
+ SysFreeString(zip_path_var.bstrVal);
+ SysFreeString(destination_dir_var.bstrVal);
+ zip_idispatch_var.pdispVal->Release();
+ zip_folderitems->Release();
+ destination_folder->Release();
+ zip_folder->Release();
+ shell->Release();
+
+ // CoUninitialize: <http://msdn.microsoft.com/en-us/library/windows/desktop/ms688715(v=vs.85).aspx>
+ CoUninitialize();
+}
+
+bool have_acceptable_python() {
+ printf("Checking for Python 2.7...");
+ //key = OpenKey(HKEY_CURRENT_USER, L"Environment", 0, KEY_QUERY_VALUE)
+ //key = OpenKey(HKEY_CURRENT_USER, L"SYSTEM\\CurrentControlSet\\Control\\Session Manager\\Environment", 0, KEY_QUERY_VALUE)
+ //...
+ return false;
+}
+
+void install_python(wchar_t *python_installer_dir) {
+ wchar_t installer_wildcard[] = L"\\*.msi";
+ wchar_t installer_pattern[MAX_PATH];
+ fail_unless(wcslen(python_installer_dir) < MAX_PATH - wcslen(installer_wildcard),
+ "Could not construct pattern for Python installer.")
+ wcscpy(installer_pattern, python_installer_dir);
+ wcscat(installer_pattern, installer_wildcard);
+
+ WIN32_FIND_DATA find_data;
+ HANDLE search_handle = FindFirstFileW(installer_pattern, &find_data);
+ fail_unless(search_handle != INVALID_HANDLE_VALUE,
+ "Could not find the Python installer.")
+
+ fail_unless(wcslen(python_installer_dir) < MAX_PATH - wcslen(find_data.cFileName),
+ "Could not construct path to Python installer.")
+ wcscpy(installer_pattern, python_installer_dir);
+ wcscat(installer_pattern, find_data.cFileName);
+ //CreateProcessW(".msi");
+}
+
+void fail(char *s) {
+ // TODO: show dialog box
+ puts(s);
+ exit(1);
+}
--- /dev/null
+<?xml version="1.0" encoding="Windows-1252"?>\r
+<VisualStudioProject\r
+ ProjectType="Visual C++"\r
+ Version="9.00"\r
+ Name="installer"\r
+ ProjectGUID="{413A02C3-FBCC-42B4-9277-F1CCBDD274CA}"\r
+ RootNamespace="installer"\r
+ Keyword="Win32Proj"\r
+ TargetFrameworkVersion="196613"\r
+ >\r
+ <Platforms>\r
+ <Platform\r
+ Name="Win32"\r
+ />\r
+ </Platforms>\r
+ <ToolFiles>\r
+ </ToolFiles>\r
+ <Configurations>\r
+ <Configuration\r
+ Name="Debug|Win32"\r
+ OutputDirectory="$(SolutionDir)$(ConfigurationName)"\r
+ IntermediateDirectory="$(ConfigurationName)"\r
+ ConfigurationType="1"\r
+ CharacterSet="1"\r
+ >\r
+ <Tool\r
+ Name="VCPreBuildEventTool"\r
+ />\r
+ <Tool\r
+ Name="VCCustomBuildTool"\r
+ />\r
+ <Tool\r
+ Name="VCXMLDataGeneratorTool"\r
+ />\r
+ <Tool\r
+ Name="VCWebServiceProxyGeneratorTool"\r
+ />\r
+ <Tool\r
+ Name="VCMIDLTool"\r
+ />\r
+ <Tool\r
+ Name="VCCLCompilerTool"\r
+ Optimization="0"\r
+ PreprocessorDefinitions="WIN32;_DEBUG;_CONSOLE"\r
+ MinimalRebuild="true"\r
+ BasicRuntimeChecks="3"\r
+ RuntimeLibrary="3"\r
+ UsePrecompiledHeader="2"\r
+ WarningLevel="3"\r
+ DebugInformationFormat="4"\r
+ />\r
+ <Tool\r
+ Name="VCManagedResourceCompilerTool"\r
+ />\r
+ <Tool\r
+ Name="VCResourceCompilerTool"\r
+ />\r
+ <Tool\r
+ Name="VCPreLinkEventTool"\r
+ />\r
+ <Tool\r
+ Name="VCLinkerTool"\r
+ LinkIncremental="2"\r
+ UACExecutionLevel="2"\r
+ GenerateDebugInformation="true"\r
+ SubSystem="1"\r
+ TargetMachine="1"\r
+ />\r
+ <Tool\r
+ Name="VCALinkTool"\r
+ />\r
+ <Tool\r
+ Name="VCManifestTool"\r
+ VerboseOutput="true"\r
+ />\r
+ <Tool\r
+ Name="VCXDCMakeTool"\r
+ />\r
+ <Tool\r
+ Name="VCBscMakeTool"\r
+ />\r
+ <Tool\r
+ Name="VCFxCopTool"\r
+ />\r
+ <Tool\r
+ Name="VCAppVerifierTool"\r
+ />\r
+ <Tool\r
+ Name="VCPostBuildEventTool"\r
+ Description="Making self-extracting executable..."\r
+ CommandLine="cmd /c "copy /y /b $(OutDir)\$(ProjectName).exe+C:\tahoe\allmydata-tahoe-1.10.0c1.zip $(OutDir)\$(ProjectName).exe""\r
+ />\r
+ </Configuration>\r
+ <Configuration\r
+ Name="Release|Win32"\r
+ OutputDirectory="$(SolutionDir)$(ConfigurationName)"\r
+ IntermediateDirectory="$(ConfigurationName)"\r
+ ConfigurationType="1"\r
+ CharacterSet="1"\r
+ WholeProgramOptimization="1"\r
+ >\r
+ <Tool\r
+ Name="VCPreBuildEventTool"\r
+ />\r
+ <Tool\r
+ Name="VCCustomBuildTool"\r
+ />\r
+ <Tool\r
+ Name="VCXMLDataGeneratorTool"\r
+ />\r
+ <Tool\r
+ Name="VCWebServiceProxyGeneratorTool"\r
+ />\r
+ <Tool\r
+ Name="VCMIDLTool"\r
+ />\r
+ <Tool\r
+ Name="VCCLCompilerTool"\r
+ Optimization="2"\r
+ EnableIntrinsicFunctions="true"\r
+ PreprocessorDefinitions="WIN32;NDEBUG;_CONSOLE"\r
+ RuntimeLibrary="2"\r
+ EnableFunctionLevelLinking="true"\r
+ UsePrecompiledHeader="2"\r
+ WarningLevel="3"\r
+ DebugInformationFormat="3"\r
+ />\r
+ <Tool\r
+ Name="VCManagedResourceCompilerTool"\r
+ />\r
+ <Tool\r
+ Name="VCResourceCompilerTool"\r
+ />\r
+ <Tool\r
+ Name="VCPreLinkEventTool"\r
+ />\r
+ <Tool\r
+ Name="VCLinkerTool"\r
+ LinkIncremental="1"\r
+ GenerateDebugInformation="true"\r
+ SubSystem="1"\r
+ OptimizeReferences="2"\r
+ EnableCOMDATFolding="2"\r
+ TargetMachine="1"\r
+ />\r
+ <Tool\r
+ Name="VCALinkTool"\r
+ />\r
+ <Tool\r
+ Name="VCManifestTool"\r
+ />\r
+ <Tool\r
+ Name="VCXDCMakeTool"\r
+ />\r
+ <Tool\r
+ Name="VCBscMakeTool"\r
+ />\r
+ <Tool\r
+ Name="VCFxCopTool"\r
+ />\r
+ <Tool\r
+ Name="VCAppVerifierTool"\r
+ />\r
+ <Tool\r
+ Name="VCPostBuildEventTool"\r
+ />\r
+ </Configuration>\r
+ </Configurations>\r
+ <References>\r
+ </References>\r
+ <Files>\r
+ <Filter\r
+ Name="Source Files"\r
+ Filter="cpp;c;cc;cxx;def;odl;idl;hpj;bat;asm;asmx"\r
+ UniqueIdentifier="{4FC737F1-C7A5-4376-A066-2A32D752A2FF}"\r
+ >\r
+ <File\r
+ RelativePath=".\installer.cpp"\r
+ >\r
+ </File>\r
+ <File\r
+ RelativePath=".\stdafx.cpp"\r
+ >\r
+ <FileConfiguration\r
+ Name="Debug|Win32"\r
+ >\r
+ <Tool\r
+ Name="VCCLCompilerTool"\r
+ UsePrecompiledHeader="1"\r
+ />\r
+ </FileConfiguration>\r
+ <FileConfiguration\r
+ Name="Release|Win32"\r
+ >\r
+ <Tool\r
+ Name="VCCLCompilerTool"\r
+ UsePrecompiledHeader="1"\r
+ />\r
+ </FileConfiguration>\r
+ </File>\r
+ </Filter>\r
+ <Filter\r
+ Name="Header Files"\r
+ Filter="h;hpp;hxx;hm;inl;inc;xsd"\r
+ UniqueIdentifier="{93995380-89BD-4b04-88EB-625FBE52EBFB}"\r
+ >\r
+ <File\r
+ RelativePath=".\stdafx.h"\r
+ >\r
+ </File>\r
+ <File\r
+ RelativePath=".\targetver.h"\r
+ >\r
+ </File>\r
+ </Filter>\r
+ <Filter\r
+ Name="Resource Files"\r
+ Filter="rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav"\r
+ UniqueIdentifier="{67DA6AB6-F800-4c08-8B7A-83BB121AAD01}"\r
+ >\r
+ <File\r
+ RelativePath=".\app.rc"\r
+ >\r
+ </File>\r
+ <File\r
+ RelativePath=".\logo.ico"\r
+ >\r
+ </File>\r
+ </Filter>\r
+ </Files>\r
+ <Globals>\r
+ </Globals>\r
+</VisualStudioProject>\r
--- /dev/null
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+ xmlns:h="http://www.w3.org/1999/xhtml"
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ width="256"
+ height="256"
+ preserveAspectRatio="xMidYMid"
+ viewBox="-180 -180 256 256"
+ id="svg3480"
+ version="1.1"
+ inkscape:version="0.48.4 r9939"
+ sodipodi:docname="logo.svg">
+ <metadata
+ id="metadata3511">
+ <rdf:RDF>
+ <cc:Work
+ rdf:about="">
+ <dc:format>image/svg+xml</dc:format>
+ <dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+ <dc:title>Tahoe-LAFS logo</dc:title>
+ </cc:Work>
+ </rdf:RDF>
+ </metadata>
+ <sodipodi:namedview
+ pagecolor="#ffffff"
+ bordercolor="#666666"
+ borderopacity="1"
+ objecttolerance="10"
+ gridtolerance="10"
+ guidetolerance="10"
+ inkscape:pageopacity="0"
+ inkscape:pageshadow="2"
+ inkscape:window-width="784"
+ inkscape:window-height="765"
+ id="namedview3509"
+ showgrid="false"
+ inkscape:zoom="0.921875"
+ inkscape:cx="128"
+ inkscape:cy="128"
+ inkscape:window-x="0"
+ inkscape:window-y="25"
+ inkscape:window-maximized="0"
+ inkscape:current-layer="svg3480" />
+ <style
+ type="text/css"
+ id="style3482">
+ .storage {
+ fill: black;
+ stroke: none;
+ }
+ .arrow {
+ stroke: black;
+ stroke-width: 2;
+ marker-end: url(#arrowhead);
+ }
+</style>
+ <title
+ id="title3484">Tahoe-LAFS logo</title>
+ <desc
+ id="desc3486">
+ <h:p>A proposed logo for the Tahoe-LAFS Project.</h:p>
+ <h:p><h:a
+ rel="license"
+ href="http://creativecommons.org/licenses/by/3.0/">
+ <h:img
+ alt="Creative Commons License"
+ style="border-width:0"
+ src="http://i.creativecommons.org/l/by/3.0/88x31.png" />
+</h:a>
+<h:br />
+<h:span
+ href="http://purl.org/dc/dcmitype/StillImage"
+ property="dct:title"
+ rel="dct:type">Tahoe-LAFS Logo</h:span>
+ by <h:a
+ href="http://switchb.org/kpreid/2009/tahoe/"
+ property="cc:attributionName"
+ rel="cc:attributionURL">Kevin Reid</h:a>
+ is licensed under a <h:a
+ rel="license"
+ href="http://creativecommons.org/licenses/by/3.0/">Creative Commons Attribution 3.0 Unported License</h:a>
+.</h:p>
+ </desc>
+ <defs
+ id="defs3488">
+ <marker
+ id="arrowhead"
+ viewBox="0 -5 10 10"
+ refX="2"
+ refY="0"
+ markerUnits="userSpaceOnUse"
+ markerWidth="10"
+ markerHeight="10"
+ orient="auto">
+ <path
+ d="M 0 -3 L 9 0 L 0 3 z"
+ id="path3491" />
+ </marker>
+ </defs>
+ <g
+ transform="translate(-48 28) scale(3.4)"
+ id="g3493">
+ <line
+ class="arrow"
+ x1="0"
+ y1="0"
+ x2="0"
+ y2="-35"
+ id="line3495" />
+ <line
+ class="arrow"
+ x1="0"
+ y1="0"
+ x2="-8"
+ y2="-21"
+ id="line3497" />
+ <line
+ class="arrow"
+ x1="0"
+ y1="0"
+ x2="8"
+ y2="-21"
+ id="line3499" />
+ <circle
+ fill="red"
+ r="10"
+ id="circle3501"
+ d="M 10,0 C 10,5.5228475 5.5228475,10 0,10 -5.5228475,10 -10,5.5228475 -10,0 c 0,-5.5228475 4.4771525,-10 10,-10 5.5228475,0 10,4.4771525 10,10 z" />
+ <circle
+ class="storage"
+ r="8"
+ cy="-50"
+ id="circle3503"
+ d="m 8,-50 c 0,4.418278 -3.581722,8 -8,8 -4.418278,0 -8,-3.581722 -8,-8 0,-4.418278 3.581722,-8 8,-8 4.418278,0 8,3.581722 8,8 z" />
+ <circle
+ class="storage"
+ r="8"
+ cy="-35"
+ cx="-14"
+ id="circle3505"
+ d="m -6,-35 c 0,4.418278 -3.581722,8 -8,8 -4.418278,0 -8,-3.581722 -8,-8 0,-4.418278 3.581722,-8 8,-8 4.418278,0 8,3.581722 8,8 z" />
+ <circle
+ class="storage"
+ r="8"
+ cy="-35"
+ cx="14"
+ id="circle3507"
+ d="m 22,-35 c 0,4.418278 -3.581722,8 -8,8 -4.418278,0 -8,-3.581722 -8,-8 0,-4.418278 3.581722,-8 8,-8 4.418278,0 8,3.581722 8,8 z" />
+ </g>
+</svg>