package WWW::Github::Files;
use strict;
use warnings;
use LWP::UserAgent;
use JSON qw{decode_json};
use Carp;

our $VERSION = 0.13;

sub new {
    my ($class, %options) = @_;

    die "Please pass a author name"
        unless exists $options{author};
    die "Please pass a resp name"
        unless exists $options{resp};
    die "Please pass either a branch name or a commit"
        unless exists $options{branch} or exists $options{commit};

    my $self = {};
    foreach my $key (qw( author resp token branch commit self_token )) {
        next unless exists $options{$key};
        $self->{$key} = $options{$key};
    if (not exists $self->{token}) {
        $self->{ua} = LWP::UserAgent->new();
        $self->{ua}->default_header( Authorization => "token ".$self->{self_token} )
            if exists $self->{self_token};
    $self->{apiurl} = ''.$options{author}.'/'.$options{resp};
    bless $self, $class;

sub open {
    my ($self, $path) = @_;
    croak("Path should start with '/'! |$path|")
        unless $path =~ m!^/!;
    my $commit = $self->__fetch_root();
    $path =~ s!/$!!;
    $path = '/' if $path eq '';
    my $f_data = $self->geturl("/contents$path?ref=$commit");
    if (ref($f_data) eq 'ARRAY') {
        # a directory
        my ($name) = $path =~ m!/([^/]*)$!;
        my $dir = {
            FS => $self,
            content => $f_data,
            name => $name,
            path => ( $path eq '/' ? '' : substr($path, 1) ),
        return bless $dir, 'WWW::Github::Files::Dir';
    elsif ($f_data->{type} eq 'file') {
        return bless $f_data, 'WWW::Github::Files::File';
    else {
        croak('unrecognised file type for $path');

sub get_file {
    my ($self, $path) = @_;
    return $self->open($path)->read();

sub get_dir {
    my ($self, $path) = @_;
    return $self->open($path)->readdir();

sub __fetch_root {
    my $self = shift;
    my $root = $self->{root_commit};
    return $root if $root;

    if ($self->{branch}) {
        my $b_data = $self->geturl('/branches/'.$self->{branch});
        $root = $b_data->{commit}->{sha};
    else {
        my $c_data = $self->geturl('/git/commits/'.$self->{commit});
        $root = $self->{commit};
    $self->{root_commit} = $root;
    return $root;

sub geturl {
    my ($self, $url, $method) = @_;
    my $token = $self->{token} || $self->{ua};
    $method ||= 'get';
    my $res = $token->$method($self->{apiurl} . $url);
    if (!$res->is_success()) {
        if ($res->message() =~ m/Internal Server Error/) {
            # retry
            my $res2 = $token->$method($self->{apiurl} . $url);
            if ($res2->is_success()) {
                $res = $res2;
            print STDERR $res2->message(), ", ", $res2->content, "\n";
        if (!$res->is_success()) {
            die "Failed to read $self->{apiurl}$url from github: ".$res->message(). ", ".$res->content;
    my $content = $res->content;
    return decode_json($content);

package WWW::Github::Files::File;
use MIME::Base64 qw{decode_base64};

sub is_file { 1 }
sub is_dir { 0 }

sub name { return $_[0]->{name} }
sub path { return '/'.$_[0]->{path} }

sub read {
    my $self = shift;
    if (not $self->{content}) {
        # this is a file object created from directory listing. 
        # need to fetch the content
        my $f_data = $self->{FS}->open('/'.$self->{path});
        $self->{$_} = $f_data->{$_} for (qw{ encoding content });
    if ($self->{encoding} eq 'base64') {
        return decode_base64($self->{content});
    else {
        die "can not handle encoding " . $self->{encoding} . " for file ". $self->{path};

package WWW::Github::Files::Dir;

sub is_file { 0 }
sub is_dir { 1 }

sub name { return $_[0]->{name} }
sub path { return '/'.$_[0]->{path} }

sub readdir {
    my $self = shift;
    if (not $self->{content}) {
        # this is a file object created from directory listing. 
        # need to fetch the content
        my $f_data = $self->{FS}->open('/'.$self->{path});
        $self->{content} = $f_data->{content};
    my @files;
    foreach my $rec (@{ $self->{content} }) {
        $rec->{FS} = $self->{FS};
        if ($rec->{type} eq 'file') {
            push @files, bless($rec, 'WWW::Github::Files::File');
        elsif ($rec->{type} eq 'dir') {
            push @files, bless($rec, 'WWW::Github::Files::Dir');
        else {
            croak('unrecognised file type: '.$rec->{type});
    return @files;


=head1 NAME

WWW::Github::Files - Read files and directories from Github


    my $gitfiles = WWW::Github::Files->new(
        author => 'semuel',
        resp => 'site-lang-collab',
        branch => 'master',

    my @files = $gitfiles->open('/')->readdir();


Using Github API to browse a git resp easily and download files

This modules is a thin warper around the API, just to make life easier


The easiest way to get a file off Github is to use the raw url:

This will return the content of this module's MANIFEST file. Easy, but 
the file have to be public and you need to know beforehand where exactly 
it is. (this method does not fetch directory content)

Also, if you download two files under 'master', there is a chance that a
commit happened in the middle and you get two files from two different
versions of the respo. Of course you can fetch the current commit and
use it instead of master, but then it is less easy

This module let you use Access Token for permission, and scan directories


Need to write code that read files from Github and local repositories?
Check out L<WWW::Github::Files::Mock> that uses the same interface
for local directory.


=over 4

=item author - resp author

=item resp - resp name

=item branch - The branch to read from

Mutual exlusive with 'commit'. 

On first access the object will "lock" on the latest commit in this branch,
and from this point will serve files only from this commit

=item commit - a specific commit to read from

The object will retrive files and directories as they were after this commit

=item token

Optional Net::Oauth2 Access Token, for using in API calls.
If not specified, will make anonymous calls using LWP

=item self_token

Optional Github "Personal Access Token" to use for API authentication. 


=head1 METHODS

=head2 open(path)

receive path (which have to start with '/') and return file or dir object
for that location

=head2 get_file(path)

shortcut to $gitfiles->open(path)->read()

=head2 get_dir(path)

shortcut to $gitfiles->open(path)->readdir()


=head2 name

The name of the file

=head2 path

full path (+name) of the file

=head2 is_file

=head2 is_dir

=head2 read

returns the content of the file


=head2 name

The name of the directory

=head2 path

full path (+name) of the directory

=head2 is_file

=head2 is_dir

=head2 readdir

returns a list of file/dir objects that this directory contains

=head1 AUTHOR
Fomberg Shmuel, E<lt>shmuelfomberg@gmail.comE<gt>
Copyright 2013 by Shmuel Fomberg.
This library is free software; you can redistribute it and/or modify
it under the same terms as Perl itself.