Entwickler-Ecke

Internet / Netzwerk - FtpPutFile und Stream?


LittleBen - So 26.08.12 15:44
Titel: FtpPutFile und Stream?
Guten Mittag,
ich arbeite zurzeit mit den Funktionen von WinInet und zwar für FTP-Uplaods (bewusst ohne Indy). Jetzt möchte ich gerne den Inhalt eines Memos hochladen und das bestmöglich ohne eine temporäre Datei. Das geht natürlich nur mit einem Stream...Gibt es irgendeinen Weg, einen Stream mit FtpPutFile hochzuladen?


Delphi-Quelltext
1:
2:
3:
4:
5:
6:
function FtpPutFile(
    hConnect: HINTERNET;
    lpszLocalFile: PChar;
    lpszNewRemoteFile: PChar;
    dwFlags: DWORD;
    dwContext: DWORD): BOOL;


Noch einen schönen Sonntag und Grüße,
Littleben


jaenicke - So 26.08.12 16:14

Siehe Dokumentation:
http://msdn.microsoft.com/en-us/library/windows/desktop/aa384170(v=vs.85).aspx hat folgendes geschrieben:
FtpPutFile is a high-level routine that handles all the bookkeeping and overhead associated with reading a file locally and storing it on an FTP server. An application that needs to send file data only, or that requires close control over the file transfer, should use the FtpOpenFile and InternetWriteFile [http://msdn.microsoft.com/en-us/library/windows/desktop/aa385128.aspx] functions.
Einfacher und vor allem plattformunabhängig wäre es natürlich mit z.B. Indy, aber da du das nicht nutzen möchtest...

Indy setzt übrigens nicht auf die API auf sondern kommuniziert direkt mit dem FTP-Server.


LittleBen - So 26.08.12 21:13

ahhh ^^ Ok, vielen Dank!
Könntest du mir noch "kurz" sagen, was ich beim Lesevorgang falsch mache?


Delphi-Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
18:
19:
procedure UploadData(RemoteFile: string; Data: TMemoryStream);
var hOpen, hUrl: HINTERNET;
    Buffer: array[0..1024of Char;
    BytesRead: dWord;
    Data_written: Cardinal;
begin
 hOpen:= InternetOpen(PChar(FDesc), INTERNET_OPEN_TYPE_PRECONFIG, nilnil0);
 hUrl:= InternetOpenUrl(hOpen, PChar(FHost), nil0, INTERNET_FLAG_RAW_DATA, 0);

 FillChar(Buffer, SizeOf(Buffer), 0);
 repeat
  FillChar(Buffer, SizeOf(Buffer), 0);
  Data.ReadBuffer(Buffer,sizeOf(Buffer));
  InternetWriteFile(hUrl,addr(Buffer),sizeof(Buffer),Data_Written);
 until (Data_Written<=0or (data.Position>=data.Size);

 InternetCloseHandle(hurl);
 InternetCloseHandle(hOpen);
end;

Bekomme dabei den Fehler "Stream-Lesefehler"


Gerd Kayser - Mo 27.08.12 00:03

user profile iconLittleBen hat folgendes geschrieben Zum zitierten Posting springen:
Bekomme dabei den Fehler "Stream-Lesefehler"


Hast Du vielleicht vergessen, vor dem Lesen aus dem Stream die Position zu setzen?


jaenicke - Mo 27.08.12 07:49

Das wird es wohl sein, ja. Ich würde die verbleibende Datengröße im Stream aber auch beim Lesen berücksichtigen statt einfach zu versuchen so viele Bytes auszulesen wie in den Puffer passen. ;-)


SvenAbeln - Mo 27.08.12 09:01

Auch beim Schreiben mit InternetWriteFile sollte die wirkliche Anzahl an Bytes berücksichtigt werden. Im Moment schreibst du immer den kompletten Buffer, auch wenn du z.B. nur ein Byte aus dem Stream gelesen hast.


LittleBen - Mo 27.08.12 09:58

Mhm, die Position vor der Schleife zu setzten hat auch nichts gebracht...ein Seek-Befehl genau so nicht.


Delphi-Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
18:
19:
20:
procedure UploadData(RemoteFile: string; Data: TMemoryStream);
var hOpen, hUrl: HINTERNET;
    Buffer: array[0..1024of Char;
    BytesRead: dWord;
    Data_written: Cardinal;
begin
 hOpen:= InternetOpen(PChar(FDesc), INTERNET_OPEN_TYPE_PRECONFIG, nilnil0);
 hUrl:= InternetOpenUrl(hOpen, PChar(FHost), nil0, INTERNET_FLAG_RAW_DATA, 0);

 Data.Position:= 0;
 //Data.Seek(0,soFromBeginning);
 FillChar(Buffer, SizeOf(Buffer), 0);
 repeat
  FillChar(Buffer, SizeOf(Buffer), 0);
  Data.ReadBuffer(Buffer,sizeOf(Buffer));
  InternetWriteFile(hUrl,addr(Buffer),sizeof(Buffer),Data_Written);
 until (Data_Written<=0or (data.Position>=data.Size);

 InternetCloseHandle(hurl);
 InternetCloseHandle(hOpen);


SvenAbeln - Mo 27.08.12 13:11

ReadBuffer prüft ob genau die angegebene Anzahl Bytes gelesen wurde und gibt sonst einen "Stream-Lesefehler". Falls du also nicht genau 1025 Char in deinem Stream hast, wirst du so immer diesen Fehler bekommen.
Read liest einfach die Bytes und gibt dabei keinen Fehler aus.


LittleBen - Mo 27.08.12 14:17

Ok, ein Fehler bekomm ich nun nicht mehr, aber es wird komischerweise nichts ausgelesen?


Delphi-Quelltext
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:
var hOpen, hUrl: HINTERNET;
    BufferIn : INTERNET_BUFFERS;
    Buffer: array[0..1024of Byte;
    TmpStream: TMemoryStream;
    cURL: string;
    Size: integer;
    BytesWritten: cardinal;
begin
 TmpStream:= TMemoryStream.Create;
 Form1.Memo1.lines.SaveToStream(TmpStream);
 cURL:= 'http://test.de/test.dat';

 hOpen := InternetOpen('Test', INTERNET_OPEN_TYPE_PRECONFIG, nilnil0);
 hUrl := InternetOpenUrl(hOpen, PChar(cURL), nil0, INTERNET_FLAG_RAW_DATA, 0);

 try
  BufferIn.dwBufferTotal:= TmpStream.Size;
  Size:= TmpStream.Read(Buffer, 1024);
  // Size = 0
  InternetWriteFile(hUrl, @Buffer, size, BytesWritten);

  while (BytesWritten = Size) and (BytesWritten > 0do
  begin
   size:= TmpStream.Read(Buffer, 1024);
   InternetWriteFile(hUrl, @Buffer, Size, BytesWritten);
  end;
 finally
  TmpStream.Free;

  InternetCloseHandle(hUrl);
  InternetCloseHandle(hOpen);
 end;


LittleBen - Mi 29.08.12 13:55

Hier nun die funktionierende Funktion:

Delphi-Quelltext
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:
function TUploader.UploadData(Source: string; RemoteFile: string): boolean;
var hOpen, hConnect, hFile: HINTERNET;
    Size: integer;
    Index: integer;
    toWrite: cardinal;
    BytesWritten: cardinal;
const Blocksize = 1024;
begin
 hOpen:= InternetOpen(PChar(FDesc), INTERNET_OPEN_TYPE_DIRECT, nilnil0);
 hConnect:= InternetConnect(hOpen, PChar(FHost), FPort, PChar(FUsername), PChar(FPassword), INTERNET_SERVICE_FTP, INTERNET_FLAG_PASSIVE, 0);
 hFile:= FtpOpenFile(hConnect, PChar(RemoteFile), GENERIC_WRITE, FTP_TRANSFER_TYPE_BINARY, 0);
 try
  if Assigned(hFile) then
  begin
   Size:= Length(Source);
   Index:= 1;
   repeat
    toWrite:= Size - Index + 1;
    if toWrite>Blocksize then
     toWrite:= Blocksize;
    BytesWritten:= 0;
    if InternetWriteFile(hFile,@source[Index],toWrite,BytesWritten) then
     inc(Index,BytesWritten);
   until (BytesWritten < Blocksize) or (Index>Size);
   result:= true;
  end;
 finally
  InternetCloseHandle(hFile);
  InternetCloseHandle(hConnect);
  InternetCloseHandle(hOpen);
 end;
end;

Hat jemand eine Idee, wie ich den String nur hinzufügen kann ohne die Datei komplett zu überschreiben (also Append)? Erst auslesen und dann wieder komplett schreiben ist nicht ideal, da der Upload nicht gerade der schnellste ist...

SOLVED: http://support.microsoft.com/kb/182316/de


jaenicke - Mi 29.08.12 19:39

Das ganze liest sich so als ob du gar kein FTP brauchst, sondern eher ein Skript auf dem Server, das die Datei schreibt...

Dann hast du auch nicht das Problem, dass du die Logindaten mit dem Programm mitliefern musst, was bei FTP ja unumgänglich ist. (Die lassen sich da ja blitzschnell auslesen, egal was du machst.)


LittleBen - Mi 29.08.12 19:43

Mhm, stimmt. Also wäre es eine Alternative einfach via Post die Daten an eine PHP-Datei zu schicken?


jaenicke - Mi 29.08.12 20:12

Richtig. Da kann dann zumindest nicht viel passieren, wenn jemand die Datei herausfindet, er kann höchstens die Datei vollmüllen.

Zudem ist es sehr viel einfacher.


LittleBen - Mi 29.08.12 20:27

Okay, das werde ich nacher gleich mal probieren. Davor würde ich trotzdem noch gerne die Geschichte mit dem FTP zuende bringen, es ist ja eigentlich nicht mehr weit vom Ziel entfernt ^^ Der folgende Code gibt ein Error beim FtpCommand aus. Um genauer zu sein den Errorcode 12003, was folgendes bedeutet:
Zitat:
ERROR_INTERNET_EXTENDED_ERROR
Vom Server wurde ein erweiterter Fehler zurückgegeben. Hier handelt es sich in der Regel um eine Zeichenfolge oder einen Puffer, der eine ausführliche Fehlermeldung enthält.

Rufen Sie "Call InternetGetLastResponseInfo" auf, um den
Fehlertext zu erhalten.
Als InternetGetLastResponseInfo bekomme ich zurück:
Zitat:
200 Type set to A
227 Entering Passive Mode (210,202,225,203,197,39)
451 Test.dat: Append/Restart not permitted, try again
Weiß jemand, an was das liegt? Muss ich mich anderst anmelden?

CODE:

Delphi-Quelltext
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:
37:
38:
39:
40:
41:
42:
43:
44:
45:
46:
47:
48:
49:
var hOpen, hConnect, hFile: HINTERNET;
    Size: integer;
    Index: integer;
    toWrite: cardinal;
    BytesWritten: cardinal;
    dwError : DWord;
    szBuffer : PChar;
    dwSize  : DWord;
const Blocksize = 1024;
begin
 hOpen:= InternetOpen(PChar(FDesc), INTERNET_OPEN_TYPE_DIRECT, nilnil0);
 hConnect:= InternetConnect(hOpen, PChar(FHost), FPort, PChar(FUsername), PChar(FPassword), INTERNET_SERVICE_FTP, INTERNET_FLAG_PASSIVE, 0);

 if Append then
  if Assigned(hConnect) then
   if not FtpCommand(hConnect, true, FTP_TRANSFER_TYPE_ASCII, PChar('APPE '+RemoteFile), 0then
   begin
    showmessage(inttostr(GetLastError));
    dwSize:= 255;
    szBuffer:= StrAlloc(dwSize);
    try
     InternetGetLastResponseInfo(dwError, szBuffer, dwSize);
     showmessage(szBuffer);
    finally
     StrDispose(szBuffer);
    end;
   end;

 hFile:= FtpOpenFile(hConnect, PChar(RemoteFile), GENERIC_WRITE, FTP_TRANSFER_TYPE_BINARY, 0);
 try
  if Assigned(hFile) then
  begin
   Size:= Length(Source);
   Index:= 1;
   repeat
    toWrite:= Size - Index + 1;
    if toWrite>Blocksize then
     toWrite:= Blocksize;
    BytesWritten:= 0;
    if InternetWriteFile(hFile,@source[Index],toWrite,BytesWritten) then
     inc(Index,BytesWritten);
   until (BytesWritten < Blocksize) or (Index>Size);
   result:= true;
  end;
 finally
  InternetCloseHandle(hFile);
  InternetCloseHandle(hConnect);
  InternetCloseHandle(hOpen);
 end;


jaenicke - Mi 29.08.12 22:31

Du musst dies am FTP-Server konfigurieren. Standardmäßig ist das oft deaktiviert.


LittleBen - Mi 29.08.12 23:08

Mhm, ich habe als Testserver einen FTP-Server bei Ohost. Der Support behauptet es liege am Client. Man kann sonst auch nichts am Server einstellen... :(

EDIT: Hab hier noch etwas interessantes gefunden, kann es aber nicht richtig deuten ^^
Zitat:
Syntax: APPE remote-filename

Append data to the end of a file on the remote host. If the file does not already exist, it is created. This command must be preceded by a PORT or PASV command so that the server knows where to receive data from.
http://www.nsftools.com/tips/RawFTP.htm#APPE
Zitat:
Syntax: PORT a1,a2,a3,a4,p1,p2

Specifies the host and port to which the server should connect for the next file transfer. This is interpreted as IP address a1.a2.a3.a4, port p1*256+p2.
http://www.nsftools.com/tips/RawFTP.htm#PORT
Zitat:
Syntax: PASV

Tells the server to enter "passive mode". In passive mode, the server will wait for the client to establish a connection with it rather than attempting to connect to a client-specified port. The server will respond with the address of the port it is listening on, with a message like:
227 Entering Passive Mode (a1,a2,a3,a4,p1,p2)
where a1.a2.a3.a4 is the IP address and p1*256+p2 is the port number.
http://www.nsftools.com/tips/RawFTP.htm#PASV


jaenicke - Do 30.08.12 09:34

Also ich kann es auf meinem dedicated Server einstellen und auf meinem Webspace wird das unterstützt, habe es bei 3 Servern ausprobiert (nicht mit dem Quelltext, auf der Konsole). ;-)
Bei einem Freehoster brauchst du dich nicht wundern... die paar Euro würde ich schon investieren.

Aber davon abgesehen ist die andere Lösung doch in 20 Minuten fertig und sowieso sinnvoller. ;-)


jaenicke - Do 30.08.12 09:36

// EDIT: Doppelpost...


LittleBen - Do 30.08.12 11:49

Nagut, dann lass ich das mit dem FTP sein ^^ Zudem ist die Parameterliste in WinInet auch noch falsch...der letzte Parameter fehlt.
Gibt es eine begrenzte Datenmenge die man mit Post übermitteln kann? Und fallen am Server nicht noch größere Lasten an, wenn alles noch über eine PHP-Datei geht?


jaenicke - Do 30.08.12 12:26

user profile iconLittleBen hat folgendes geschrieben Zum zitierten Posting springen:
Zudem ist die Parameterliste in WinInet auch noch falsch...der letzte Parameter fehlt.
Bei Delphi 7, ja. In aktuellen nicht soweit ich das sehe. ;-)

user profile iconLittleBen hat folgendes geschrieben Zum zitierten Posting springen:
Gibt es eine begrenzte Datenmenge die man mit Post übermitteln kann?
Nicht direkt. Ein Timeout wäre möglich, wenn es zu lange dauert, je nach Servereinstellung. Größere Datenmengen würde ich aber als Dateiupload hochladen, da gehen je nach Server mind. 1 MiB, bei "echten" Hostern bis zu 30 oder so.

user profile iconLittleBen hat folgendes geschrieben Zum zitierten Posting springen:
Und fallen am Server nicht noch größere Lasten an, wenn alles noch über eine PHP-Datei geht?
PHP-Zugriffe sind das Normale, FTP-Zugriffe nicht. Deshalb ist es wohl eher umgekehrt, zumal das PHP-Skript gut optimieren kann.


LittleBen - Do 30.08.12 14:38

user profile iconjaenicke hat folgendes geschrieben Zum zitierten Posting springen:
Bei Delphi 7, ja. In aktuellen nicht soweit ich das sehe. ;-)
Mal eine ganz doofe Frage: Wie kompilier ich denn die pas neu? Eine Änderung in der WinInet wird komischerweise einfach "übersehen" :gruebel:

user profile iconjaenicke hat folgendes geschrieben Zum zitierten Posting springen:
Nicht direkt. Ein Timeout wäre möglich, wenn es zu lange dauert, je nach Servereinstellung. Größere Datenmengen würde ich aber als Dateiupload hochladen, da gehen je nach Server mind. 1 MiB, bei "echten" Hostern bis zu 30 oder so.
Ab welcher Größe würdest du "groß" sagen? Bei mir handelt es sich eig. nur um Mengen von 10 - 50 KB.

user profile iconjaenicke hat folgendes geschrieben Zum zitierten Posting springen:
PHP-Zugriffe sind das Normale, FTP-Zugriffe nicht. Deshalb ist es wohl eher umgekehrt, zumal das PHP-Skript gut optimieren kann.
Ah okay, dachte es wäre umgekehrt :)

EDIT: Hab mich bei Ohost über die Append-Funktion informiert -> "ist und bleibt abgeschaltet"


jaenicke - Do 30.08.12 17:20

user profile iconLittleBen hat folgendes geschrieben Zum zitierten Posting springen:
user profile iconjaenicke hat folgendes geschrieben Zum zitierten Posting springen:
Bei Delphi 7, ja. In aktuellen nicht soweit ich das sehe. ;-)
Mal eine ganz doofe Frage: Wie kompilier ich denn die pas neu? Eine Änderung in der WinInet wird komischerweise einfach "übersehen" :gruebel:
Das ist nicht komisch, die .dcu ist ja noch da und die Quelltextdateien auch entsprechend konfiguriert (.pas ich Suchpfad, .dcu in Bibliothekspfad). Zum Neukompilieren kopierst du die am besten in deinen Projektordner und fügst die Unit dem Projekt hinzu. Empfehlenswert ist das aber ganz und gar nicht. Besser ist es nur selektiv die zu ändernde Funktion in deinem Projekt zu implementieren.
Sonst muss man das ja bei jeder Delphiversion erneut anpassen...

user profile iconLittleBen hat folgendes geschrieben Zum zitierten Posting springen:
Ab welcher Größe würdest du "groß" sagen? Bei mir handelt es sich eig. nur um Mengen von 10 - 50 KB.
Bei den Datenmengen ist das im Grunde egal, aber wenn es sich nicht um reine Textdateien handeln sollte, is der Upload als Datei schon wegen der Zeichenkodierung sinnvoll.

user profile iconLittleBen hat folgendes geschrieben Zum zitierten Posting springen:
EDIT: Hab mich bei Ohost über die Append-Funktion informiert -> "ist und bleibt abgeschaltet"
Freehoster halt ;-)
Ob sich das für die 2 Euro im Monat oder so lohnt, muss jeder selbst wissen. ;-)


LittleBen - Do 30.08.12 17:48

Stimmt! Es einfach nur im Projekt zu implementieren ist besser :)
Einen "richtigen" FTP-Server habe ich (bei Hosteurope), wollte ihn für dieses Projekt aber nicht benutzen (aus den Sicherheitsgründen, die du genannt hast). Hat sich jetzt aber geklärt.
Vielen, vielen Dank für deine Hilfe!