Kirjautuminen

Haku

Tehtävät

Keskustelu: Koodit: PHP: FTP-client luokka

Turatzuro [31.07.2005 21:29:04]

#

Tässä on perustoiminnot osaava FTP-luokka. PHP:ssa on toki jo itsessäänkin valmiina ftp-funktiot, mutta koska esimerkiksi omalla servullani niiden käyttö on kielletty, rakensin socketeilla toimivan ratkaisun. Tukee passiivista siirtoa, koska aktiiviseen tarvittavat portit ovat harvemmin servuissa auki. Tämän käytössä on muuten sitten onkelmana se, että PHP:n oma aikaraja tulee yleensä luvattoman nopeasti vastaan, varsinkin kopioitaessa jotain isompaa.

Koodin alla esimerkkiä käytöstä.

Muokattu 2.8.2005:
- Pari viivettä, yleisiä koodin selvennyksiä
- Uusi metodi: palvelin_info()
- Nyt palvelin määritetään konstruktorissa

FTP-luokka

<?php
class ftp_client {
	var $kayttaja = '';
	var $salasana = '';
	var $palvelin = '';

	var $debug = array();
	var $katkess = true;	// Piilottaa salasanan debugeista

	var $yht_socket = false; // Socket alkuyhteyttä varten
	var $dat_socket = false; // Socket datansiirtoa varten

	var $luettu_data = '';
	var $wdir = "/";

	// Asettaa yhteysasetukset
	// Argumentit: Palvelimen osoite, käyttäjätunnus, salasana
	// Palauttaa: True tai false
	function ftp_client($palvelin, $kayttaja = 'anonymous', $salasana = 'anonymous@') {
		if(empty($palvelin)) {
			$this->debug[] = "Palvelinta ei määritelty";
		} else {
			$this->kayttaja = $kayttaja;
			$this->salasana = $salasana;
			$this->palvelin = $palvelin;
			$this->debug[] = "Tunnus, salasana sekä palvelin määritetty.\n";
			return true;
		}
		return false;
	}

	// Tällä luetaan monta riviä serverinlähettämää palautetta
	// Palauttaa: Luettu data tai false
	function lue_kasa() {
		if($this->yht_socket !== false) {
			$i=0;
			while($i<100) { // Jos ei tule kamaa ajoissa, oletetaan yhteys kuolleeksi.
				$data = fgets($this->yht_socket);
				if($data === false || empty($data)) {
					$i++;
					usleep(250000); // Odotellaan 0,25 sekuntia ennenkuin yritetään lukea uudelleen
				}
				else {
					while(1) { // Luetaan niin kauan kuin kamaa tulee
						$data2 = fgets($this->yht_socket);
						if(empty($data2) || $data2 == false) {
							break;
						} else {
							$data .= $data2;
						}
					}
					$this->debug[] = '<b>Luetaan:</b> '.str_replace("\n","\n\t ",$data);
					$this->luettu_data = $data;
					return $data;
				}
			}
			$this->debug[] = "Ei voitu lukea, Timeout!\n";
		} else {
			$this->debug[] = "Yhteys ei ole auki!\n";
		}
		return false; // oletetaan että servu tukossa tms.
	}

	// Tällä lukaistaan vain rivi servun lähettämää palautetta
	// Palauttaa: luettu data tai false
	function lue_rivi() {
		if($this->yht_socket !== false) {
			$i=0;
			while($i<100) { // Jos ei tule kamaa ajoissa, oletetaan yhteys kuolleeksi.
				$data = fgets($this->yht_socket);
				if($data === false || empty($data)) {
					$i++;
					usleep(250000); // Odotellaan 0,25 sekuntia ennenkuin yritetään lukea uudelleen
				}
				else {
					$this->debug[] = '<b>Luetaan:</b> '.str_replace("\n","\n\t ",$data);
					return $data;
				}
			}
			$this->debug[] = "Ei voitu lukea, Timeout!\n";
		} else {
			$this->debug[] = "Yhteys ei ole auki!\n";
		}
		return false; // oletetaan että servu tukossa tms.
	}

	// Luetaan Binääri tai Ascii-muotoista dataa datakanavasta
	// Palauttaa: true tai false
	function lue_data() {
		if($this->dat_socket !== false) {
			$data = '';
			while(!feof($this->dat_socket)) { // Luetaan niin kauan kuin kamaa tulee
				$data .= fread($this->dat_socket,1024); // Napsitaan paketin verran tavaraa puskurista
			}
			$this->debug[] = "<b>Luetaan</b> datakanavasta\n";
			return $data;
		} else {
			$this->debug[] = 'Datakanavaa ei ole luotu!';
		}
	}

	// Kirjoitetaan dataa kontrollikanavaan (21)
	// Argumentit: Kirjoitettava data
	// Palauttaa: true tai false
	function kirjoita($data) {
		if($yht_socket !== false) {
			$rr = substr($data,0,4);
			if($rr == 'PASS' && $this->katkess)
				$this->debug[] = '<b>Kirjoit:</b> PASS ********';
			else
				$this->debug[] = '<b>Kirjoit:</b> '.$data;
			@fwrite($this->yht_socket,$data."\n");
			return true;
		} else {
			$this->debug[] = 'Yhteys ei ole auki!';
		}
		return false;
	}

	// Kirjoitetaan dataa datakanavaan
	// Argumentit: Kirjoitettava data
	// Palauttaa: true tai false
	function kirjoita_data($data) {
		if($dat_socket !== false) {
			@fwrite($this->dat_socket,$data);
			return true;
		} else {
			$this->debug[] = 'Datakanavaa ei ole luotu!';
		}
		return false;
	}

	// Tällä tarkistetaan toiminnon (epä)onnistuminen.
	// Argumentit: oikea vastaus, luetaanko vain rivi
	// Palauttaa: true tai false
	function testaa($testattava,$rivi=false)
	{
		$data = (!$rivi) ? $this->lue_kasa() : $this->lue_rivi();

		if(!$data || substr($data,0,3) != $testattava) // Jos ei ole dataa tai tulee virhe...
			return false; // ... niin palautetaan false
		else
			return true;

	}

	// Tällä luodaan varsinainen aloitusyhteys (kontrollikanava)
	// Palauttaa: true tai false
	function yhdista() {
		$this->yht_socket = @fsockopen($this->palvelin,21,$errno,$errstr,15);
		@set_socket_blocking($this->yht_socket,false); // Ei jäädä odottelemaan dataa
		if(!$this->yht_socket) {
			$this->debug[] = "Virhe $errno yhdistettäessä. <i>$errstr</i>\n";
		} else {
			usleep(250000);
			if($this->testaa(220)) // Luetaan tervetulotoivotus, tarkastetaan yhteys
			{
				$this->kirjoita('USER '.$this->kayttaja); // Kirjoitetaan käyttäjätunnus
				if($this->testaa(331)) // Luetaan vastaus, Tarkastetaan, että homma meni putkeen
				{
					$this->kirjoita('PASS '.$this->salasana); // Kirjoitetaan salasana
					if($this->testaa(230)) { // Tarkastetaan vastaus
						$this->debug[] = "Yhteys on luotu.\n";
						return true;
					} else {
						$this->debug[] = "Salasana väärin.\n";
					}
				} else {
					$this->debug[] = "Käyttäjätunnusta ei löydy\n";
				}
			}
		}
		return false;
	}

	// Tällä luodaan datayhteys
	// Argumentit: Siirtomoodi (TYPE A = ASCII-siirto. TYPE I = Binääri)
	// Palauttaa: true tai false
	function tee_passiivi($type = 'A') {
		if($this->dat_socket !== false) fclose($this->dat_socket);
		if($this->yht_socket !== false) {
			$this->kirjoita("TYPE $type");
			if(!$this->testaa(200)) $this->debug[] = 'Ei voitu määrittää yhteystyyppiä.';
			$this->kirjoita('PASV'); // Luodaan passiiviyhteys
			if($this->testaa(227)) { // Saatiin vastaukseisi datakanavan osoite sekä portti tyyliin (127,0,0,1,34,123)
				$ekasulku = strpos($this->luettu_data,'(')+1;
				$tokasulku = strpos($this->luettu_data,')');
				$na = substr($this->luettu_data,$ekasulku,$tokasulku-$ekasulku); // Leikataan pois turha kama
				$na = explode(',',$na); // Laitetaan osiin pillkujen kohdalta
				$portti = ($na[4]*256)+$na[5];
				$osoite = $na[0].'.'.$na[1].'.'.$na[2].'.'.$na[3]; // Hostin IP-osoite

				$this->dat_socket = fsockopen($osoite,$portti,$errno,$errstr,15);
				if(!$this->dat_socket) {
					$this->debug[] = "Virhe $errno yhdistettäessä. <i>$errstr</i>\n";
				} else {
					$this->debug[] = "Datakanava on luotu osoitteeseen $osoite:$portti\n";
					return true;
				}
			} else {
				$this->debug[] = 'Ei voitu luoda datakanavaa; ei vastausta isännältä tai virheellinen vastaus.';
			}
		} else {
			$this->debug[] = 'Yhteys ei ole auki; ei voida luoda datakanavaa.';
		}
		return false;
	}

	// Tällä voidaan kopioida kamaa serveriltä.
	// Argumentit: ladattavan tiedoston nimi, siirtomoodi (A/I)
	// Palauttaa true tai false
	function lataa($polku,$mode = 'I') {
		if(!empty($polku)) {
			if($this->tee_passiivi($mode)) {
				$this->debug[] = "Yritetään Lukea tiedosto <i>$polku</i>.\n";
				$this->kirjoita("RETR $polku");
				if($this->testaa(150,true)) {
					$rtn = $this->lue_data();
					if($this->testaa(226)) $this->debug[] = "Siirto suoritettu onnistuneesti.\n";
					return $rtn;
				} else {
					$this->debug[] = "Tiedostoa tai kansiota ei ole olemassa.\n";
					return false;
				}
			} else {
				return false;
			}
		} else {
			$this->debug[] = "Ei mitään luettavaa.\n";
			return false;
		}
	}

	// Tällä siirretään dataa serverille.
	// Argumentit: Tiedosto johon siirretään, siirrettävä data, siirtomoodi(A/I) sekä Append (siirretäänkö data olemassaolevan tiedoston perään)
	// Palauttaa true tai false
	function tallenna($polku, $data = ' ', $mode = 'I', $append = false) {
		if(!empty($data) && !empty($polku)) {
			if($this->tee_passiivi($mode)) {
				$this->debug[] = "Yritetään kirjoittaa tiedostoon <i>$polku</i>.\n";

				if($append) $this->kirjoita("APPE $polku");
				else $this->kirjoita("STOR $polku");

				if($this->testaa(150,true)) {
					$return = $this->kirjoita_data($data);
					if($this->testaa(226)) $this->debug[] = "Siirto suoritettu onnistuneesti.\n";
					return $return;
				} else {
					$this->debug[] = "Ei voitu kirjoittaa.\n";
					return false;
				}
			} else {
				return false;
			}
		} else {
			$this->debug[] = "Ei mitään kirjoitettavaa.\n";
			return false;
		}
	}

	// Tällä siistitään tiedostolistaus taulukkomuuttujaan
	// Argumentit: Listaus tekstimuodossa
	// Palauttaa: Tiedostolistauksen taulukossa
	function parseta_listaus($listaus) {
		$data = explode("\n",$listaus);
		$array;
		for($i=0; $i<count($data); $i++) {
			if(!empty($data[$i])) {
				$temp = array();
				$c = array();
				$t = 0;
				$line = preg_split("/ /",$data[$i],20,PREG_SPLIT_NO_EMPTY);

				// Oikeudet
				$temp['dir'] = (substr($line[0],0,1) == 'd') ? 1 : 0;
				$c[] = substr($line[0],1,3);
				$c[] = substr($line[0],4,3);
				$c[] = substr($line[0],7,3);
				$temp['oik'] = "";
				foreach($c as $d) {
					$arvo = 0;
					if($d[0] == 'r') $arvo += 4;
					if($d[1] == 'w') $arvo += 2;
					if($d[2] == 'x') $arvo += 1;
					$temp['oik'] .= $arvo;
				}

				// Muuta
				$temp['time'] = (strlen($line[7]) > 4)
					? (strtotime($line[6].' '.$line[5].' '.date('Y'))+substr($line[7],0,2)*60*60+substr($line[7],-2)*60)
					: strtotime($line[6].' '.$line[5].' '.$line[7]);
				$temp['gid'] = $line[2];
				$temp['uid'] = $line[3];
				$temp['size'] = $line[4];
				$temp['name'] = $line[8];

				$array[] = $temp;
			}
		}
		return $array;
	}

	// Tällä listataan kansion sisältö
	// Argumentit: listattava hakemisto
	// Palauttaa: false tai hakemiston sisältö taulukossa.
	function listaa($polku = '')
	{
		if($this->tee_passiivi())
		{
			$this->kirjoita("LIST $polku"); // Annetaan listauskomento

			if($this->testaa(150,true)) { // Tarkastetaan, ettei virhettä. Jos, palautetaan false.
				$array = array();
				$data = $this->lue_data(); // Luetaan tiedostolistaus DATAKANAVASTA !
				if($this->testaa(226)) $this->debug[] = "Siirto suoritettu onnistuneesti\n";
				return $this->parseta_listaus($data);
			} else {
				$this->debug[] = "Datayhteys hylätty\n";
				return false;
			}
		} else {
			return false;
		}
	}

	// Hakee työskentelykansion
	// Palauttaa: false tai tyskentelykansio
	function sijainti() {
		$this->kirjoita('PWD'); // Kerrotaan mihin kansioon siirrytään
		if($this->testaa(257)) {
			$polku = explode(' ',$this->luettu_data);
			$polku = str_replace('"','',$polku[1]);
			$this->debug[] = "Kansio on $polku\n";
			$this->wdir = $polku;
			return $polku;
		} else {
			$this->debug[] = "Ei voitu selvittää kansiota";
		}
		return false;
	}

	// Muuttaa työskentelykansiota
	// Argumentit: Uusi tyskentelyhakemisto
	// Palauttaa: false / uusi hakemisto
	function siirry($polku = '/') {
		$this->kirjoita("CWD $polku"); // Kerrotaan mihin kansioon siirrytään
		if($this->testaa(250)) {
			$this->debug[] = "Siirrytty kansioon $polku\n";
			return $this->sijainti();
		}
		return false;
	}

	// Luo kansion
	// Argumentit: Luotavan kansion nimi
	// Palauttaa: true tai false
	function tee_kansio($polku) {
		if(!empty($polku)) {
			$this->kirjoita("MKD $polku");
			if($this->testaa(257)) {
				$this->debug[] = "Luotu kansio $polku\n";
				return true;
			}
		}
		return false;
	}

	// Poistaa kansion
	// Argumentit: poistettavan kansion nimi
	// Palauttaa: true tai false
	function poista_kansio($polku) {
		if(!empty($polku)) {
			$this->kirjoita("RMD $polku");
			if($this->testaa(250)) {
				$this->debug[] = "Poistettu kansio $polku\n";
				return true;
			}
		}
		return false;
	}

	// Poistaa tiedoston
	// Argumentit: Poistettavan tiedoston nimi
	// Palauttaa: true tai false
	function poista_tiedosto($polku) {
		if(!empty($polku)) {
			$this->kirjoita("DELE $polku");
			if($this->testaa(250)) {
				$this->debug[] = "Poistettu tiedosto $polku\n";
				return true;
			}
		}
		return false;
	}

	// Muuttaa tiedoston tai kansion oikeuksia
	// Argumentit: Käsiteltävä kansio tai tiedosto, uudet oikeudet numeroarvona
	// Palauttaa: true tai false
	function oikeudet($polku, $val = 644) {
		$this->kirjoita("SITE CHMOD ".intval($val)." $polku");
		if($this->testaa(200)) {
			$this->debug[] = "Annettu tiedostolle $polku oikeudet $val\n";
			return true;
		}
		return false;
	}

	// Hakee palvelimen tiedot
	// Palauttaa: Tiedot taulukkona tai false
	function palvelin_info() {
		$this->kirjoita("SYST");
		if($this->testaa(215)) {
			$this->debug[] = "Järjestelmätiedot noudettu.\n";
			$jarjestelma = explode(' ',$this->luettu_data);
			return array('os' => $jarjestelma[1], 'type' => $jarjestelma[3]);
		}
		return false;
	}

	// Sulkee kontrolli sekä datakanavan
	function sulje() {
		$this->kirjoita('QUIT'); // Ilmoitetaan palvelimelle yhteyden sulkemisesta
		$this->lue_kasa(); // Luetaan vastaus
		@fclose($this->yht_socket); // Suljetaan yhteys-socker
		@fclose($this->dat_socket); // Suljetaan mahd. aukioleva data-socket
		$this->debug[] = 'Yhteys suljettu.';
	}

	// Tulostaa debugit
	function tulosta_debug() {
		$buffer = "<pre>\n";
		foreach($this->debug as $b)
			$buffer .= "$b\n";
		$buffer .= "\n</pre>";
		return $buffer;
	}
}
?>

Esimerkki

<?php
$ftp = new ftp_client('palvelin','tunnus','salasana');
$ftp->yhdista(); // Yhdistetään

$tiedot = $ftp->palvelin_info(); // Palauttaa palvelimen tiedot
$lista = $ftp->listaa(); // Tiedostolistaus
$lataus = $ftp->lataa('/kuva.jpg'); // Ladataan tiedoston sisältö
$ftp->siirry('/test'); // siirrytään kansioon
$ftp->tallenna('tiedosto.txt', 'ABUBAEOB', 'A'); Tallennetaan tiedostoon kamaa
$ftp->poista_tiedosto('tiedosto.txt'); // Poistetaan tiedosto
$ftp->tallenna('tiedosto.txt', '...jatkoa', 'A', true); // Tallennetaan olemassa olevaan tiedostoon
$ftp->tee_kansio('HAHAA'); // Luodaan kansio
$ftp->oikeudet('HAHAA',644); // Annetaan kansiolle 644-oikeudet
$ftp->poista_kansio('HAHAA'); // Poistetaan kansio

$ftp->sulje(); // Suljetaan yhteys
print $ftp->tulosta_debug(); // Tulostetaan debug-tiedot
?>

tsuriga [01.08.2005 13:26:50]

#

Jahas luokkia, jees. Mukavaa tietysti, että näitä suomeksikin lokalisoidaan, mutta käytännöllisempää olisi pitäytyä englannissa, suurempi ihmismäärä potentiaalisesti hyötyy skriptistä ja suuri osa opetusmateriaalistakin kun on englanniksi niin siinä tulisi harjoiteltua sanat sun muut.

Ja sitten itse koodista, hyvältä näyttää hyvältä näyttää. Mietin tässä, notta onkohan PHP:ssä luokille konstruktoria, tuo set-funktio olisi kätevä muuttaa sellaiseksi ilman parametrien default-arvoja.

Turatzuro [01.08.2005 13:43:58]

#

Kun kerran laitoin suomenkieliselle sivulle, niin mielestäni oli vaan asiallista tehdä koodikin suomeksi. Luultavasti näitä löytyy kyllä netistä englanniksikin, mikäli sellaisen haluaa.

Juu, kyllähän PHP:sta löytyy constructorit, ainakin versiosta 4 lähtien. Itse jostain syystä pidän tätä ratkaisua parempana, mutta eipä liene hankala korjata.

Antti Laaksonen [01.08.2005 15:58:03]

#

Hyvännäköinen luokka. Runsaampi välien käyttö parantaisi mielestäni koodisi luettavuutta. Esim. "$portti = ($na[4]*256)+$na[5];" olisi vähän selvemmin "$portti = ($na[4] * 256) + $na[5];". Lisäksi "'DELE '.$polku" olisi vähän lyhemmin ""DELE $polku"".

Minusta suomenkielisten nimien käyttäminen on hyvä tapa. PHP:ssä voi käyttää jopa ääkkösiä nimissä. Joka tapauksessa kannattaa kirjoittaa kaikki nimet samalla kielellä.

ajv [02.08.2005 11:59:25]

#

Todella mukavannäköistä koodia ja myös erikoisuudesta plussaa. Kielivalinnasta olen tsurigan kanssa samoilla linjoilla. PHP:n funktiot ovat jokatapauksessa englanninkielisiä, joten koodista ei saa suomenkielistä tekemälläkään.

tsuriga [02.08.2005 12:19:46]

#

Njuu vaan eiköhän laiteta tuonne keskustelun puolelle kielikeskustelu taas vireille joskus niin ei täällä eksytä itse asiasta :).

Ajattelin default-arvottoman konstruktorin olevan käytännöllisempi, koska funetin ftp on vain yksi ties kuinka monesta ftp:stä, ja epähuomiossa käyttäjä voi vahingossa yhdistää nyt funetille halutessaan jotain ihan muuta. Jos default-arvoja ei olisi, luokkaa luodessa tulisi virheilmoitus ja käyttäjä tietäisi korjata asian pikimmiten. Eikä tarvitsisi kutsua tuota hieman harhaanjohtavankin nimistä 'set'-funktiota.

Turatzuro [02.08.2005 15:09:00]

#

Nyt on sitten set-funktio bititaivaassa, ja sen tilalla uusi ja uljas konstruktori. Jätin silti tuonne default-tunnuksen ja salasanan, että pääsee vähemmällä vaivalla julkisille servuille. Onhan se tunnuksien kirjoittelu niin vaivalloista ;)

Vastaus

Aihe on jo aika vanha, joten et voi enää vastata siihen.

Tietoa sivustosta